当前位置: 首页 > news >正文

收纳用品网站建设北京室内设计公司排名榜

收纳用品网站建设,北京室内设计公司排名榜,建立时间和保持时间,家居装修公司排名文章目录 1. 简介1.1 TCP 协议是什么1.2 TCP 协议的作用1.3 什么是“面向连接” 2. 简述 TCP2.1 封装和解包2.2 TCP 报文格式2.3 什么是“面向字节流”2.4 通过 ACK 机制实现一定可靠性 3. 详述 TCP3.1 基本认识TCP 报头格式16 位源/目标端口号32 位序列号*32 位确认应答号4 位… 文章目录 1. 简介1.1 TCP 协议是什么1.2 TCP 协议的作用1.3 什么是“面向连接” 2. 简述 TCP2.1 封装和解包2.2 TCP 报文格式2.3 什么是“面向字节流”2.4 通过 ACK 机制实现一定可靠性 3. 详述 TCP3.1 基本认识TCP 报头格式16 位源/目标端口号32 位序列号*32 位确认应答号4 位首部长度4/6 位保留位*8/6 位控制位*16 位窗口大小其他字段校验和16 位紧急指针选项注意 序列号和确认应答号小结 3.2 连接的建立如何理解“连接”三次握手可靠性标志位RSTPSHURG TCP 的状态三次握手半连接和全连接队列 再次理解“三次握手”阻止重复历史连接的初始化同步双方初始序列号避免资源浪费 3.3 重传机制超时重传机制快速重传 3.4 连接的断开四次挥手常见问题测试 3.5 流量控制滑动窗口*滑动窗口的原理 3.6 拥塞控制拥塞窗口慢启动拥塞避免拥塞发生发生超时重传的拥塞发生算法发生快速重传的拥塞发生算法快速恢复 3.7 延迟应答3.8 捎带应答3.9 面向字节流3.10 粘包问题3.11 TCP 异常情况 4. TCP 小结小结TCP 定时器理解传输控制协议Socket 编程相关问题Accept Listen 参考资料 [重要] 本文默认读者已经体系地学习过操作系统。 为了读者能更好地学习 TCP 协议本文首先简单介绍 TCP 协议是啥然后再简述 TCP 的主要内容干嘛的最后再阐述 TCP 的各个细节原理。 1. 简介 1.1 TCP 协议是什么 与 UDP 不同TCPTransmission Control Protocol则“人如其名”可以说是对“传输、发送、通信”进行“控制”的“协议”。 TCP 与 UDP 的区别相当大。它充分地实现了数据传输时各种控制功能可以进行丢包时的重发控制还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。此外TCP 作为一种面向有连接的协议只有在确认通信对端存在时才会发送数据从而可以控制通信流量的浪费由于 UDP 没有连接控制所以即使对端从一开始就不存在或中途退出网络数据包还是能够发送出去。 在 UDP 中由应用层划分的数据包在网络中分发的「顺序」取决于网络中的「路由选择」是难以确定的。 1.2 TCP 协议的作用 TCP 协议是在不可靠的网络环境中提供可靠的数据传输服务而设计的。它解决了以下几个问题 数据丢失由于网络故障、拥塞、错误或攻击数据包可能在传输过程中丢失或损坏。TCP 协议通过序列号、确认号、校验和、重传机制等方法保证了数据的完整性和正确性。数据乱序由于网络的异构性、路由的动态变化、分片的不同顺序等原因数据包可能以不同的顺序到达接收方。TCP 协议通过序列号、确认号、缓冲区等方法保证了数据的有序性和连续性。数据重复由于网络延迟、重传机制、路由变化等原因数据包可能被发送或接收多次。TCP 协议通过序列号、确认号、滑动窗口等方法避免了数据的重复性和冗余性。流量控制由于发送方和接收方的处理能力和网络带宽可能不匹配发送方可能会发送过多的数据导致接收方或中间节点的缓冲区溢出。TCP 协议通过滑动窗口、停止-等待等方法根据接收方的反馈调整发送方的发送速率防止了缓冲区溢出和数据丢失。拥塞控制由于网络中的节点或链路可能超过其承载能力导致网络拥塞和性能下降。TCP 协议通过慢启动、拥塞避免、快速重传、快速恢复等方法根据网络状况动态调整发送方的拥塞窗口避免了网络拥塞和数据丢失。 这些问题将被 TCP 在一定程度上解决。 1.3 什么是“面向连接” 连接是指各种设备、线路或网络中进行通信的两个应用程序为了相互传递消息而专有的、虚拟的通信线路也叫做虚拟电路。 一旦建立了连接进行通信的应用程序只使用这个虚拟的通信线路发送和接收数据就可以保障信息的传输。应用程序可以不用顾虑提供尽职服务的 IP 网络上可能发生的各种问题依然可以转发数据。TCP 则负责控制连接的建立、断开、保持等管理工作。 注意“端对端”中的“端”指的是主机上特定 端 口号对应的进程。 面向连接是 TCP 的一种特性它意味着在数据传输之前两个通信实体必须建立一个连接 。这个连接是由一系列的握手消息来建立的它们用于协商连接的参数如序号、窗口大小和最大报文段长度。面向连接的目的是保证数据的可靠传输即数据按照正确的顺序、完整性和无差错地到达目的地。面向连接也使得 TCP 能够实现流量控制和拥塞控制以适应网络的状况。 2. 简述 TCP 2.1 封装和解包 封装将应用层传来的数据分割成一个个的报文段每个报文段都有一个序号和一个校验和。TCP 在发送端将报文段封装成 IP 数据报加上源地址和目的地址然后通过网络层发送到目的地。解包TCP 在接收端将 IP 数据报解封装提取出报文段根据序号和校验和来检查报文段的完整性和顺序。如果报文段有损坏或丢失TCP 会发送重传请求要求发送端重新发送报文段。如果报文段没有问题TCP 会将其放入接收缓冲区并按照序号排序。当接收缓冲区中有一定数量的连续报文段时TCP 会将它们传递给应用层。根据当前网页内容这就是 TCP 进行解包和交付的过程。 如何确定缓冲区 我们知道 端口号是 TCP 报文段中的一个字段它用于标识发送端和接收端的应用程序。套接字是一种抽象的数据结构它由 IP 地址和端口号组成用于表示网络上的一个通信点。文件描述符是操作系统为每个打开的文件或设备分配的一个整数它可以用于读写文件或设备。 TCP 在建立连接时会为每个连接分配一个套接字对即一个源套接字和一个目的套接字。这个套接字对就是 TCP 连接的唯一标识。TCP 在接收端会根据报文段中的源地址、源端口、目的地址和目的端口来匹配相应的套接字对然后将报文段放入该套接字对对应的接收缓冲区。 TCP 在传递数据给应用层时会根据应用层请求的套接字来从相应的接收缓冲区中取出数据。因此缓冲区是由套接字来确定的而不是由文件描述符来确定的。文件描述符和套接字之间有一种映射关系即每个文件描述符都可以对应一个套接字但不是每个套接字都可以对应一个文件描述符。 2.2 TCP 报文格式 就报文格式而言TCP 比 UDP 复杂得多下文结合 TCP 报文格式阐述 TCP 是如何「解包」的其他组成部分的功能将在第三节「详述」部分阐述。 TCP 的报头是变长的包括固定的 20 字节和变长的选项。其中“数据偏移”也叫做“首部长度”它占固定 4 位作用是保存报头整体的长度以便接收端能够正确解析报文中的字段。值得注意的是虽然首部长度占 4 位但是它的单位是 1 个字节那么 4 个比特位能表示的范围 0~15就能表示 0~60 字节。 图中每一行有 4 个字节解包步骤如下 提取报头 除了选项之外的报头叫做标准报头一共 20 字节。提取选项根据 4 位首部长度获取报头的整体大小减去 20 字节的标准报头得到选项。如果没有选项的话就能直接得到有效载荷。 提取有效载荷有效载荷 报文-报头 (-选项 注意TCP 连接是由以下四个属性四元组唯一确认的 源 IP 地址发送数据的主机的 IP 地址。源端口号发送数据的应用程序的端口号通常是一个随机分配的临时端口号。目标 IP 地址接收数据的主机的 IP 地址。目标端口号接收数据的应用程序的端口号通常是一个预先定义的固定端口号。 这四个属性组成了一个套接字socket也就是 TCP 连接的端点。一条 TCP 连接由两个套接字唯一确定也就是通信双方的地址和端口信息。 所以『端对端』从操作系统的角度理解是进程从代码实现的角度来看就是 socket因为 socket 的实现 bind 了端口号。 四元组协议 五元组可以唯一确认某一协议的连接。 2.3 什么是“面向字节流” 由于 TCP 面向字节流所以它不一定每次都能接收到未被分割的数据因此不需要判定报文之间的边界。 这句话的意思是TCP 协议在传输数据时不会保留数据的边界信息也就是说发送方发送的数据可能会被拆分或合并成不同的 TCP 报文段接收方收到的数据也可能是不完整或多个数据拼接在一起的。因此接收方不能根据 TCP 报文段来判断数据的完整性和顺序而需要自己定义一些规则来区分不同的数据。 简单地说“流”就像水龙头中的水我们要接一桶水可以一次性接满也可以分批次接。 TCP 是面向字节流的协议与 UDP 是面向报文的协议相对应。UDP 协议在传输数据时会保留数据的边界信息也就是说发送方发送的数据就是一个 UDP 报文接收方收到的数据也是一个 UDP 报文每个报文都是一个完整的数据单元。 面向字节流和面向报文的区别主要在于上层应用程序如何看待 TCP 和 UDP 的传输方式 对于 TCP 来说数据是以字节为单位连续地传输的没有任何结构或边界的概念。对于 UDP 来说数据是以报文为单位分别传输的每个报文都有自己的边界和长度。 从代码实现来看面向字节流就相当于这些数据都由一个字符数组保存。 2.4 通过 ACK 机制实现一定可靠性 ACK Acknowledgement到达确认 机制是指 TCP 在接收端收到报文段后会发送一个确认报文段ACK给发送端表示已经收到了某个序号的报文段。发送端收到 ACK 后会更新自己的发送窗口表示可以继续发送更多的报文段。 通常两个人对话时在谈话的停顿处可以点头或询问以确认谈话内容。如果对方迟迟没有任何反馈说话的一方还可以再重复一遍以保证对方确实听到。因此对方是否理解了此次对话内容对方是否完全听到了对话的内容都要靠对方的反应来判断。网络中的“确认应答”就是类似这样的一个概念。当对方听懂对话内容时会说“嗯”这就相当于返回了一个确认应答ACK。而当对方没有理解对话内容或没有听清时会问一句“咦”这好比一个否定确认应答NACKNACKNegative Acknowledgement 。 TCP 通过肯定的确认应答ACK实现可靠的数据传输。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答说明数据已经成功到达对端。反之则数据丢失的可能性很大。 如果发送端在一定时间内没有收到 ACK它会认为报文段丢失或延迟然后重新发送报文段。这样TCP 可以保证数据不会因为网络故障而丢失。 需要强调的是ACK 机制并不能保证数据的顺序和完整性也就是说TCP 仅靠 ACK 机制是无法完全保证可靠性的。如果报文段到达的顺序和发送的顺序不一致或者报文段被篡改或损坏ACK 机制就无法检测出来。 因此TCP 还需要其他的机制来保证可靠性如序号机制、校验和机制、重传超时机制、累积确认机制、选择性确认机制等。 此处的“窗口”即下文将着重介绍的“滑动窗口”。 通过上面两张图可以体会到 ACK 机制很像现实生活中人们之间交流的过程这个比喻是很恰当的事实上“通信”这件事的主体只不过是从人变成了机器通信过程中的各种细节还是类似的。实际上TCP 包括目前主流的网络通信协议都是基于 ACK 机制来实现可靠的数据传输的。只不过不同协议会根据具体需求有不同的细节和优化。 值得注意的是之所以图示中表示报文传输的箭头总是斜的是因为数据不管在网络还是在机器内部传输不论路程有多短都需要消耗一定时间。这就像子弹不论多快都不可能以直线运动一样。 [了解] 为什么要让 TCP 提供可靠性其他层次的协议不可以吗 让 TCP 提供可靠性是因为 TCP 是运输层的一个协议而运输层的主要功能之一就是为上层的应用层提供可靠的 端到端 的数据传输服务。 其他层次的协议也可以提供可靠性但是可能会有一些问题或者限制。 应用层的协议可以在自己的层次上实现可靠性例如 FTP、HTTP 等但是这样会增加应用层的复杂度和开销而且可能会和运输层的可靠性机制冲突或者重复。网络层的协议可以提供可靠性例如 IPsec 等但是这样会增加网络层的负担和延迟而且可能会和运输层的可靠性机制冲突或者重复。链路层的协议可以提供可靠性例如 PPP、ATM 等但是这样只能保证链路之间的可靠性而不能保证 端到端的可靠性而且可能会和运输层的可靠性机制冲突或者重复。 因此在互联网协议栈中让 TCP 提供可靠性是一种比较合理和高效的设计选择它可以为上层应用提供一个可靠的字节流服务而不需要关心下层网络的细节和不确定性。 3. 详述 TCP 3.1 基本认识 TCP 报头格式 在第二节中简单介绍了 TCP 报头中的 4 位首部长度数据偏移下面将介绍其他部分。 [注] 标*的为重点 了解即可 TCP 的报头在 Linux 内核中属于 struct tcphdr 数据类型该类型定义在 linux/tcp.h 文件中。TCP 的报头包含了一些字段其中 6 个标志位URG、ACK、PSH、RST、SYN、FIN是用来表示 TCP 的控制信息的它们本质上是 位域/位段即用一个字节或者一个字中的某些位来表示一个变量。 TCP 的报头的结构如下 struct tcphdr {__be16 source; // 源端口号__be16 dest; // 目的端口号__be32 seq; // 序列号__be32 ack_seq; // 确认号 #if defined (__LITTLE_ENDIAN_BITFIELD)__u16 res1:4, // 保留位doff:4, // 数据偏移表示报头长度fin:1, // FIN 标志位表示结束连接syn:1, // SYN 标志位表示请求建立连接rst:1, // RST 标志位表示重置连接psh:1, // PSH 标志位表示推送数据ack:1, // ACK 标志位表示确认收到数据urg:1, // URG 标志位表示紧急数据ece:1, // ECE 标志位表示显式拥塞通知回应cwr:1; // CWR 标志位表示拥塞窗口减少 #elif defined (__BIG_ENDIAN_BITFIELD)__u16 doff:4, // 数据偏移表示报头长度res1:4, // 保留位cwr:1, // CWR 标志位表示拥塞窗口减少ece:1, // ECE 标志位表示显式拥塞通知回应urg:1, // URG 标志位表示紧急数据ack:1, // ACK 标志位表示确认收到数据psh:1, // PSH 标志位表示推送数据rst:1, // RST 标志位表示重置连接syn:1, // SYN 标志位表示请求建立连接fin:1; // FIN 标志位表示结束连接 #else #error Adjust your asm/byteorder.h defines #endif __be16 window; // 窗口大小__sum16 check; // 校验和__be16 urg_ptr; // 紧急指针指示紧急数据的位置 };这是一个结构体其中的一些字段是位段。位段是一种用来节省空间的数据结构它可以用一个字节或者一个字中的某些位来表示一个变量。 例如TCP 报头中的标志位字段就是用一个 16 位的字中的 6 个位来表示 6 个不同的变量每个变量只占 1 位。 16 位源/目标端口号 源端口号Source Port表示发送端端口号字段长 16 位。 目标端口号Destination Port表示接收端端口号字段长度 16 位。 32 位序列号 在建立连接时由计算机生成的随机数作为其初始值通过 SYN 包传给接收端主机每发送一次数据就 累加 一次该 数据字节数 的大小。 作用由于请求很可能不止一个而且通信的任意一方接收到的报文中都含有序号所以要用序号给每个请求标号以待条件允许时只要对其排序就可以实现有序地回应解决网络包乱序问题。 *32 位确认应答号 指下一次 应该收到 的数据的序列号。即在 2.4 节中简述的 ACK 机制。 实际上它是指已收到确认应答号减一为止的数据。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。 作用解决丢包问题。例如 2.4 中的第一个例子对端主机发送了 1~1000 的数据那么收到数据的一端就要发送 1001 的确认应答号表示 1001 之前的数据已经被成功接收。 确认应答号非常重要如果它的值是 x那么发送 x 的一端要传达的信息就是 我已经收到了 x 之前注意是之前的数据 。如果发送数据的一端收不到 x 或者收到的 x 和预期的不一样可能是上次的那么它会认为接收数据的一端没有成功接收到数据即发生了丢包此时发送数据的一端就会重新发送数据。这样发送数据的一端就能按照确认应答传达的信息继续发送下一段数据以保证数据不被丢失。 4 位首部长度 首部长度表示 TCP 所传输的数据部分应该从 TCP 包的哪个位开始计算看作 TCP 首部的长度。该字段长 4 位单位为 4 字节即 32 位。 不包括选项字段 的话TCP 的首部规定为 20 字节长因此首部长度字段可以设置为 5。反之如果该字段的值为 5那说明从 TCP 包的最一开始到 20 字节为止都是 TCP 首部余下的部分为 TCP 数据。 4/6 位保留位 暂时不用关心。 该字段主要是为了以后扩展时使用其长度一般为 4 位。一般设置为 0但即使收到的包在该字段不为 0此包也不会被丢弃保留字段的第 4 位如下图中的第 7 位用于实验目的相当于 NSNonce Sum标志位。 。 *8/6 位控制位 字段长为 8 位每一位从左至右分别为 CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。这些控制标志也叫做控制位。当它们对应位上的值为 1 时具体含义如图所示。 [注] 如上所述 如果 TCP 首部没有选项Options字段那么数据偏移字段的值就是 5表示 TCP 首部长度为 20 字节。这时保留位占 6 位控制位占 6 位。 如果 TCP 首部有选项字段那么数据偏移字段的值就大于 5表示 TCP 首部长度大于 20 字节。这时保留位占 4 位控制位占 8 位。 因此有的书里 TCP 的保留位是 4 位控制位是 8 位有的是 6 位保留位控制位是 6 位都是正确的只是根据不同的情况来解释数据偏移字段而已。 下面要介绍的是 TCP 首部没有选项字段的情况即保留位占 6 位控制位占 6 位去除了 8 和 9 位CWR 和 ECE。 服务端可能会随时收到来自不同客户端的报文所以报文中要 携带标志位区分报文的类型实际上它们都是宏。 其中有三个标志位是关于『请求报文』的即建立和断开连接的过程中所必须设置的 SYNSynchronize Flag表示该报文是一个 建立连接的请求报文。SYN 为 1 表示希望建立连接并在其序列号的字段进行序列号初始值的设定Synchronize 本身有同步的意思。也就意味着建立连接的双方序列号和确认应答号要保持同步。。 FINFin Flag该位为 1 时表示 本端 今后不会再有数据发送是一个 断开连接的请求报文。当通信结束希望断开连接时通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。每个主机又对对方的 FIN 包进行确认应答以后就可以断开连接。不过主机收到 FIN 设置为 1 的 TCP 段以后不必马上回复一个 FIN 包而是可以等到缓冲区中的所有数据都因已成功发送而被自动删除之后再发。 ACKAcknowledgement Flag该位为 1 时确认应答 的字段变为有效。只要报文具有『应答特征』那么它就应该被设置为 1。TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1。细节会在『ACK 机制』中介绍。 SYN、FIN 和 ACK 标志位都可以与其他标志位组合使用例如 SYNACK 表示对连接请求的确认并且也请求建立连接。 值得注意的是 SYN 和 FIN 标志位都需要对方的确认而 ACK 标志位本身就是一种确认。SYN 和 FIN 标志位都会改变 TCP 连接的状态而 ACK 标志位不会。SYN 标志位只会出现在建立连接的『三次握手』过程中FIN 标志位只会出现在终止连接的四次挥手过程中而 ACK 标志位会出现在整个 TCP 通信过程中。简单地说 SYN只出现在连接建立阶段ACK出现在整个通信阶段FIN只出现在断开连接阶段。 下面是用来处理 TCP 协议中不同属性的数据的三个标志位它们都是 建立连接之后 才会使用的标志位它们不会出现在三次握手或四次挥手的过程中 PSHPush Flag该位为 1 时告知接收端应用程序应该立刻将 TCP 接收缓冲区中的数据读走而不是等待缓冲区满了再向上交付。当 PSH 为 0 时则不需要立即传而是先进行缓存。PSH 标志位可以提高数据的及时性适用于实时性要求较高的应用例如 SSH 和 Telnet。 RSTReset Flag该位为 1 时表示 TCP 连接中出现异常必须强制断开连接。RST 标志位可以用于拒绝非法的报文段或者拒绝连接请求也可以用于在连接发生错误时快速释放资源。 URGUrgent Flag该位为 1 时表示包中有需要紧急处理的数据。对于需要紧急处理的数据会结合后面的『紧急指针』。紧急指针字段指示了紧急数据在报文段中的位置。URG 标志位可以提供一种类似于带外数据的传输方式适合于传输一些异常或重要的信息如中断或终止命令等。 对于这后三个标记位应该结合 TCP 的握手过程理解。 *16 位窗口大小 由于服务端在任何时候都可能接收来自不同客户端发送的数据因此服务端 接收 数据的能力是有限的而且是实时变化的。所以客户端就要以合适的速率传输数据给服务端这 取决于服务端的接收缓冲区中剩余空间的大小 。类似地客户端 发送 数据的能力也是有限的。 速度的快慢是相对的这取决于通信双方的发送能力和接收能力。举个例子新老师在上课时经常会问同学们讲课的速度这是因为新老师需要知晓同学接收信息的能力。类似地在上网课时老师会时不时说“听懂打 1”通过老师 请求 -学生 反馈 的方式获取学生的接收能力。 如何保证发送方用合适的流量发送 服务端在响应时给客户端同步自己的接收能力。如何告知对方呢 报头中的 16 位窗口大小指的是接收端当前可以接收的数据量双方进行报文交换的过程就是报头交换的过程。参与通信的任意一方都可能会发送或接收数据那么在发送数据时应该将报头中的窗口大小填写为 自己 可以接收的数据量的大小。 值得注意的是窗口大小是指接收端当前可以接收的数据量它并不一定等于当前可变缓冲区的剩余大小。因为接收端可能会根据网络状况或者应用需求动态地调整自己的接收窗口大小而不是简单地根据缓冲区的剩余大小来设置。例如将会使用后续要介绍的『拥塞控制』算法限制窗口大小等。 关于窗口大小的具体作用将在后续的『滑动窗口』中介绍。 其他字段 剩下的字段包括校验和、紧急指针和选项。在此学习时可以最后再补充它们在此仅做介绍。 下文大部分内容引用自《图解 TCP/IP》 校验和 校验和checksum是用来 检测 TCP 报头和数据是否有错误的它占 16 位是对报头和数据的所有字节求和后取反得到的。发送端在发送报文段时会计算校验和并填充在报头中接收端在收到报文段时会重新计算校验和并与报头中的值比较如果不相等说明报文段有错误需要丢弃或者重传。 源 IP 地址与目标 IP 地址在 IPv4 的情况下都是 32 位字段在 IPv6 地址时都为 128 位字段。填充是为了补充位数时用一般填入 0。 TCP 的校验和与 UDP 相似区别在于 TCP 的校验和无法关闭。 TCP 和 UDP 一样在计算校验和的时候使用 TCP 伪首部。这个伪首部如上图所示。为了让其全长为 16 位的整数倍需要在数据部分的最后填充 0。首先将 TCP 校验和字段设置为 0。然后以 16 位为单位进行 1 的补码和计算再将它们总和的 1 的补码和放入校验和字段。 接收端在收到 TCP 数据段以后从 IP 首部获取 IP 地址信息构造 TCP 伪首部再进行校验和计算。由于校验和字段里保存着除本字段以外其他部分的和的补码值因此如果计算校验和字段在内的所有数据的 16 位和以后得出的结果是“16 位全部为 11 的补码中该值为 0负数 0、二进制中为 1111111111111111十六进制中为 FFFF十进制中则为正整数 65535。 ”说明所收到的数据是正确的。 使用校验和的目的是什么 有噪声干扰的通信途中如果出现位错误可以由数据链路的 FCS 检查出来。那么为什么 TCP 或 UDP 中也需要校验和呢 其实相比检查噪声影响导致的错误TCP 与 UDP 的校验和更是一种进行路由器内存故障或程序漏洞导致的数据是否被破坏的检查。 有过 C 语言编程经验的人都知道如果指针使用不当极有可能会破坏内存中的数据结构。路由器的程序中也可能会存在漏洞或程序异常宕掉的可能。在互联网中发送数据包要经由好多个路由器一旦在发送途中的某一个路由器发生故障经过此路由器的包、协议首部或数据就极有可能被破坏。即使在这种情况下TCP 或 UDP 如果能够提供校验和计算也可以判断协议首部和数据是否被破坏。 16 位紧急指针 紧急指针urgent pointer是用来处理紧急数据的它占 16 位 只有当 URG 标志位被设置时才有效 它表示紧急数据在报文段中的位置。发送端在发送紧急数据时会设置 URG 标志位并填充紧急指针接收端在收到 URG 标志位时会根据紧急指针找到紧急数据并优先处理。 如何处理紧急数据 如何处理紧急数据属于应用的问题。一般在暂时中断通信或中断通信的情况下使用。例如在 Web 浏览器中点击停止按钮或者使用 TELNET 输入 Ctrl C 时都会有 URG 为 1 的包。此外紧急指针也用作表示数据流分段的标志 选项 选项options是用来扩展 TCP 功能的用于提高 TCP 的传输性能。它是可选的可以占 0 到 320 位一般是 32 位的整数倍这取决于数据偏移首部长度。选项可以用来设置一些参数或者协商一些特性例如最大报文段长度MSS、窗口缩放因子WSF、选择性确认SACK等。选项一般在 TCP 连接建立时交换也可以在数据传输过程中使用。 注意 紧急指针并不常用也不太可靠。 紧急指针只能表示一个字节的位置而不是一个数据块的范围而且不同的操作系统对紧急指针的处理方式也不一致有些会将紧急数据单独传递给应用层有些会将紧急数据与普通数据混合在一起。因此在实际应用中很少使用紧急指针来传输重要或异常的信息而更多地使用其他的方式例如单独的信道或者应用层协议。 相比之下校验和和选项可能更值得关注因为它们对 TCP 的可靠性和性能有很大的影响。校验和可以保证 TCP 报文段的完整性和正确性选项可以提供一些高级功能和优化策略。 通过序列号和 ACK 机制提高可靠性 在 2.4 中说明了 ACK 机制能够提高可靠性但仅靠 ACK 机制是无法完全实现可靠性的。言外之意是TCP 为了实现它的可靠性采取了若干措施付出了很多代价。 其中之一就是序列号配合 ACK 机制在 2.4 中的例子中我只说明了『确认延迟到达』的一种情况即主机 A 发送的数据发生了丢包导致主机 B 无法接收数据也就无法发送确认应答。 还有一种情况是主机 B 收到了主机 A 发送的数据但是主机 B 发送的确认应答发生了丢包。两种情况对于主机 A 都是一样的发送了数据却收不到确认应答。 此外也有可能因为一些其他原因导致确认应答延迟到达在源主机重发数据以后才到达的情况也履见不鲜。不论如何发送了数据却收不到确认应答源发送主机只要按照机制重发数据即可。 对于但是对于目标主机来说这简直是一种“灾难”。它会反复收到相同的数据。而为了对上层应用提供可靠的传输必须得放弃重复的数据包。为此就必须引入一种机制它能够识别是否已经接收数据又能够判断是否需要接收。 上述这些确认应答处理、重发控制以及重复控制等功能都可以通过 序列号 实现。序列号是按顺序给发送数据的每一个字节8 位字节都标上号码的编号。接收端查询接收数据 TCP 首部中的序列号和数据的长度将自己下一步应该接收的序号作为确认应答返送回去。就这样通过序列号和确认应答号TCP 可以实现可靠传输。 其中 序列号或确认应答号也指字节与字节之间的分隔。 TCP 的数据长度并未写入 TCP 首部。实际通信中求得 TCP 包的长度的计算公式是IP 首部中的数据包长度-IP 首部长度 TCP 首部长度。 MSSMaximum Segment Size报文最大长度在建立 TCP 连接的同时也可以确定发送数据包的单位我们也可以称其为“ 最大消息长度 ”MSS。最理想的情况是最大消息长度正好是 IP 中不会被分片处理的最大数据长度这样就省去了分片和重组的成本。 TCP 在传送大量数据时是 以 MSS 的大小将数据进行分割 发送。进行 重发 时也是以 MSS 为单位。 MSS 是在三次握手的时候在两端主机之间被计算得出。两端的主机在发出建立连接的请求时会在 TCP 首部中写入 MSS 选项告诉对方自己的接口能够适应的 MSS 的大小为附加 MSS 选项TCP 首部将不再是 20 字节而是 4 字节的整数倍。如下图所示的4。 。然后会在两者之间选择一个较小的值投入使用在建立连接时如果某一方的 MSS 选项被省略可以选为 IP 包的长度不超过 576 字节的值IP 首部 20 字节TCP 首部 20 字节MSS 536 字节。 。 因此TCP 是以『段』Segment为单位发送数据的和 MSS 对应。 序列号和确认应答号 序列号的作用是标记数据的顺序那么确认号有什么作用 序列号由发送数据的一方发出而确认号由接收数据的一方发出确认号告诉发送数据的一方我已经接收到了你这次发送的数据请你从这个序号的位置继续发送。 其中1001 和 2001 都是确认号。它们表示的意义是 确认号之前 的数据都已收到这样便能保证数据的完整性如果网络条件良好的话。 注意它们的单位是字节这恰好和字符数组的单位大小相同。 序列号和确认号存在的原因是要保证 TCP 是 全双工 的即主机 A 在接收主机 B 的数据的同时也要给主机 B 发送它自己要发送的数据往往是主机 A 的应答。就像生活中吵架一样边吵边听互不影响既可以收也可以发。 因此TCP 是一个 双向 的字节流协议也就是说每个方向上都有一个独立的字节流和序列号空间。因此对于任意一方来说它既有自己的序列号也有对方的确认序号它既有自己发送的报文段也有对方发送的报文段。 不论是请求还是应答本质上对于任意一方都是报文报文在“字节流”的意义下是一个字符数组那么序列号就相当于数组的下标确认序号就是数组未被使用的最新的位置。 小结 序列号和确认序号的作用 将请求和应答一一对应起来确认序号表示的是它之前的数据已经全部收到允许部分确认应答丢失或者不发送确认应答保证了 TCP 的全双工通信。 3.2 连接的建立 如何理解“连接” 我们知道TCP 在『端对端』之间建立的信道为上层『端』对应的进程提供服务它由客户端和服务端的套接字socket以及它们之间交换的数据包segment组成。TCP 连接的建立、维持和终止都需要遵循一定的协议和状态机制。另外中心化的 Client-Server 模式使得大量不同的 Client 将会与同一台 Server 建立连接那么 Server 端势必要对这些来源不同的连接进行管理。 从数据结构的角度理解我们知道TCP 是处于传输层的协议也就是说TCP 的各种逻辑由操作系统特指 Linux维护那么这些数据就得按照操作系统的规则组织即先描述后组织。 现在我们知道了这些连接在操作系统眼里只不过内核中的数据结构类型通过结构体组织当连接成功被建立时内存中就会创建对应的『连接对象』。管理不同的连接即对这些连接对象进行增删查改等操作。 既然组织连接相关的数据结构需要操作系统维护那么维护是需要成本的主要是CPU 和内存资源。这是许多网络攻击方式的切入点。 当然TCP 连接需要维护一些状态信息和参数例如序号、确认号、窗口大小、重传计时器等。这些信息和参数被存储在一个称为传输控制块Transmission Control BlockTCB的数据结构中。每个 TCP 连接都有一个唯一的 TCB 与之对应操作系统用一张表来存储所有的 TCB。TCB 中的信息和参数会随着连接的状态变化而更新。 为什么说在学习网络之前一定要先学好操作系统呢 最重要的原因就如刚才所说两个具有代表性的协议TCP 和 UDP 都是传输层的协议而传输层由操作系统内核维护那么协议的实现必须符合操作系统中的规则。 另外在 Linux 中传输控制块Transmission Control BlockTCB和线程控制块Thread Control BlockTCB或者进程控制块Process Control BlockPCB之间的关系是不同的它们分别属于不同的层次前者是传输层后两者是内核它们之间的联系是 一个进程可以创建多个线程这些线程共享进程的资源如内存空间、文件描述符等。因此线程控制块中有一个指针指向所属进程的进程控制块。 一个进程可以创建多个线程这些线程共享进程的资源如内存空间、文件描述符等。因此线程控制块中有一个指针指向所属进程的进程控制块³。 一个进程或者线程可以创建多个套接字这些套接字用于与其他进程或者线程进行通信。因此进程控制块或者线程控制块中有一个文件描述符表其中包含了指向套接字对应传输控制块的指针。 三次握手 TCP 不像 UDP 一样不检查通信信道是否正常而直接向网络中发送数据它会在数据通信之前通过 TCP 首部发送一个 SYN 包作为建立连接的请求等待确认应答为了描述的方便通常将 TCP 中发送第一个 SYN 包的一方叫做客户端接收这个的一方叫做服务端 。 如果对端发来确认应答则认为可以进行数据通信。 如果对端的确认应答未能到达就不会进行数据通信。 从客户端-服务端模式的角度来看TCP 连接的建立需要经过三次握手three-way handshake的过程即 [连接请求 A] 客户端向服务端发送一个 SYN 包请求方向 A-B 的连接[连接请求 B响应 A] 服务端收到后回送一个 SYNACK 包表示方向 B-A 的连接请求并同意建立 A-B 连接[响应 B] 客户端再发送一个 ACK 包确认连接成功。 这样双方就建立了一个可靠的、双向的、基于字节流的连接。三次握手的图示如下。 这些包使用 TCP 首部用于控制的字段来管理 TCP 连接建立一个 TCP 连接需要发送 3 个包形象的称为“三次握手”。 注意 图中虽然以 SYN 等标记位请求和应答包括下文常用标志位代替报文但实际上两端交换的是报文而不是标记位。报文可能携带数据也可能只含有报头。理论上在建立连接时每个报文都应该有回应就像打电话一样在奇数次握手中最后一个报文在连接建立之前一定没有回应的。客户端和服务端都要向对方发送建立连接的请求SYN并且需要接收到对方的确认应答ACK后才能认为『这个方向』的通信信道建立成功。这是因为 TCP 要实现『全双工』通信就必须要保证双方通信的信道是畅通的。有的时候把这个建立连接的过程叫做“四次挥手”这是因为这种说法把第二次握手 ACKSYN 拆分成了两次握手实际上都是一样的。 为啥一个信道不能保证『全双工』呢 这个问题和『加锁』的问题非常类似。我们知道要保证一个临界资源的读写一致性就要保证在每次读或写时只有一个线程或进程对其操作否则会出现数据异常。 那么如果我们读和写的部分互不干扰的话还会出现这样的问题吗 答案是不会也就是说只要我们将读和写的粒度降到尽可能小使得它们没有交集那么在保证数据一致性的同时还能保证一定的效率不过这有一定难度因为读写的区域往往是变化的。 不过『全双工』的实现只需要靠读写两个缓冲区即可。 为什么是 3 次握手而不是 1 次、2 次、4 次 这是一个经典的问题有很多不同的解释和角度。可以从几个方面来回答在这里仅从效率角度讨论在『再次理解“三次握手”中』会从多个角度回答这个问题。 为什么不能用 1 次或 2 次呢 如果只用 1 次那么客户端发送一个 SYN 后就认为连接建立成功但是如果这个 SYN 丢失了或者被延迟了那么服务器端就无法知道客户端的请求也无法给客户端发送数据。除此之外每次连接都会占用服务端一定的 CPU 和内存资源只用 1 次握手就认为建立连接成功那么当服务端在短时间内接收到大量 SYN 连接请求会造成服务端异常即 SYN 洪水攻击。如果只用 2 次那么客户端发送一个 SYN 后服务器端回复一个 SYNACK 后就认为连接建立成功但是如果这个 SYNACK 丢失了或者被延迟了那么客户端就无法知道服务器端的响应也无法给服务器端发送数据。这种情况下如果客户端重复地向服务端发送 SYN 请求也会造成服务端的 SYN 洪水。 *从连接失败的成本来说如果是 3 次握手客户端和服务端互相发送报文时主动建立连接的一方是第一个发送 SYN 报文和最后一个发送 ACK 的一方。那么客户端建立连接的时机会比服务端更靠后。也就是说建立连接的双方发出和收到的报文数量都是相等的这样 SYN 洪水攻击也就失效了因为三次握手会让发出 SYN 的一方即服务端接收等量的 ACK 响应当最后一次 ACK 没有被成功接收时失败的成本就会嫁接到客户端这样服务端就能承担最小程度的连接失败成本。 那么为什么不能用 4 次或更多呢其实从理论上讲用 4 次或更多也是可以的只要最后一次是客户端发送一个 ACK 给服务器端就行为啥因为要嫁接连接失败成本即 5/7/9 次。… 但是这样做没有必要因为第三次握手已经足够保证双方的同步和确认信息了再多发送一次或多次只会增加网络开销和延迟。也就是说三次握手是验证双方通信信道连接成功的最小次数。 TCP 的三次握手主要是为了在保证连接可靠性和双向性的同时尽量减少网络开销和延迟。 此外TCP 的三次握手嫁接连接失败成本的限度是有限的因为攻击者的机器可能会有很多如果攻击者使用病毒感染世界各地的机器操纵它们在同一时刻向同一台服务器发送仅仅几次连接请求这样失败的成本对于每台发送请求的主机而言只是几个毫无作用报文甚至比打开浏览器访问一个网页的成本还要低而被攻击的服务器如果 CPU 和内存不够强大的话会承受不住压力而出现异常。这就是 DDoS分布式拒绝服务攻击。因此 TCP 采取了更多保护措施例如黑白名单过滤策略等等。 值得注意的是TCP 的三次握手并不能保证连接可靠性下面这一节会介绍它要解决的问题有两个 嫁接连接失败成本验证全双工通信主要即保证两个方向的通信信道通畅。 三次握手的目的不仅在于让通信双方了解一个连接正在建立还在于利用数据包中的选项来传递信息。 可靠性 尽管 TCP 依靠各种办法使得连接成功的可能性尽可能高但是三次握手并不能 100%保证双方通信信道连接成功这是因为三次握手中的前两次握手能确保一定被对端接收到而第三次握手是无法知晓它是否成功被接收的。原因在于此时服务端可能会出现宕机、关机等不可预测的行为导致第三次握手的 ACK 无法正常被服务端接收也就是丢包这样连接就会建立失败。 第一次和第二次握手丢包不需要担心因为如果发送报文的一方在一定时间内没有收到对方的反馈就会重新发送报文。 实际上不存在 100%可靠的网络协议但是 TCP 能够在『局部』以最大限度地保证可靠性。『局部』从通信的距离理解就是『端到端』的距离言外之意是当通信的距离物理上很长时网络协议难以保证其可靠性。 这是因为任何经由某种介质的通信行为都可能受到干扰、丢包、延迟等影响这是一个从数学和物理上都无法解决的两军问题。 TCP 在局部保证了 100%的可靠性是因为它通过一系列机制保证数据能够保序、无差错、不重复地从一端传输到另一端。 一个很常见的例子游戏厂商往往会在各地架设服务器以供玩家选择最短距离的服务器这样延迟能尽可能低丢包率也会比较稳定。加速器也是类似的原理有些服务器离玩家很远那么加速器充当着跳板的角色间接地缩短了两者的距离。 标志位 RST 在客户端发送第三个报文即 ACK 报文后客户端此时可能会直接向对端发送数据报文但由于这个 ACK 报文是没有应答的因此如果服务端未收到 ACK 报文时服务端认为连接出现异常会返回一个含有异常标志位的报头信息 RST。 仅做举例实际上发生类似情况的概率很小。因为客户端发送数据时也会携带 ACK 标记位。 PSH 让优先级更高的报文先被处理。 URG 这里的『指针』不应该局限于语言层面上的指针实际上只要能表示『方向』都可以叫做指针。紧急指针表示的是一个位置但是 16 位只能表示一个地址它本质上是一个偏移量。 TCP 的状态 上文简要介绍了 TCP 三次握手的过程以及三次握手的原理既然第三个报文 ACK 无法收到应答那么什么时候才算连接建立成功呢这就需要用各种状态表示当前 TCP 连接以对应不同的操作和响应。 友情链接TCP 的 11 种状态 TCP 的状态有 11 种分别是 CLOSED初始状态表示 TCP 连接是“关闭着的”或“未打开的”。LISTEN表示服务器端的某个 SOCKET 处于监听状态可以接受客户端的连接。SYN_SENT表示客户端已发送 SYN 报文请求建立连接。SYN_RCVD表示服务器收到了客户端的 SYN 报文并回复了 SYNACK 报文等待客户端的确认。ESTABLISHED表示 TCP 连接已经成功建立双方可以进行数据传输。FIN_WAIT_1表示主动关闭连接的一方已发送 FIN 报文等待对方的 ACK 或 FIN 报文。FIN_WAIT_2表示主动关闭连接的一方已收到对方的 ACK 报文等待对方的 FIN 报文。CLOSE_WAIT表示被动关闭连接的一方已收到对方的 FIN 报文等待本地用户的连接终止请求。CLOSING表示双方同时发送了 FIN 报文但是主动关闭连接的一方没有收到对方的 ACK 报文等待对方的 ACK 报文。LAST_ACK表示被动关闭连接的一方已发送 FINACK 报文等待对方的 ACK 报文。TIME_WAIT表示主动关闭连接的一方已收到对方的 FINACK 报文并回复了 ACK 报文等待足够的时间以确保对方收到 ACK 报文。 三次握手 TCP 的状态在三次握手中的变化是这样的 图片和描述来自小林 codingTCP 三次握手过程是怎样的 注意 第三次握手可以携带数据前两次握手不能携带数据。因为它是一个普通的 TCP 确认报文段它的 ACK 标志位被设置为 1表示对服务端的 SYNACK 报文段的确认。如果客户端有数据要发送它可以在这个报文段中携带数据而不必等待服务端发送数据。 这么做的好处是可以提高传输效率减少网络延迟。否则就要等待服务端发送数据后才能发送它自己的数据这样就增加了一个往返时间。 不过TCP 的第三次握手是否能够携带数据取决于服务端是否支持否则可能会造成网络拥塞和重传。 图中的箭头指向的状态交界处是有原因的状态改变的时机只在发出或接收到报文。 回答本节的问题 只有双方都处于 ESTABLISHED 状态才能认为 TCP 的连接是成功的双方才能正常发送数据。TCP 的第三次握手发送的 ACK 报文是没有响应的因为它只是用来确认对方的 SYNACK 报文而不是用来请求建立连接。 对于客户端而言一旦发送了这个 ACK 报文后它就处于 ESTABLISHED 状态因为它已经完成了三次握手的过程。对于服务端而言只有当它收到了这个 ACK 报文以后才会处于 ESTABLISHED 状态因为它需要等待客户端的确认才能确定连接已经建立。 这样服务端和客户端在 TCP 的连接成功的认知上存在着时间差如果服务端并未收到第三次握手发送的 ACK 报文会出现什么情况 服务端的 TCP 连接状态为 SYN_RECV并且会根据 TCP 的『超时重传机制』会等待 3 秒、6 秒、12 秒后重新发送 SYNACK 包以便客户端重新发送 ACK 包。客户端在接收到 SYNACK 包后就认为 TCP 连接已经建立状态为 ESTABLISHED。如果此时客户端向服务端发送数据服务端将以 RST 包响应用于强制关闭 TCP 连接。如果服务端收到客户端重发的 ACK 包会先判断全连接队列是否已满如果未满则从半连接队列中拿出相关信息存放入全连接队列中之后服务端 accept() 处理此请求。如果已满则根据 tcp_abort_on_overflow 参数的值决定是扔掉 ACK 包还是发送 RST 包给客户端。 半连接和全连接队列 tcp_abort_on_overflow 是一个布尔型参数当服务端的监听队列满时新的连接请求会有两种处理方式一是丢弃二是拒绝连接通过向服务端发送 RST 报文实现。通过哪种方式处理取决于这个参数 tcp_abort_on_overflow 为 0丢弃服务端发送的 ACK 报文不建立连接。tcp_abort_on_overflow 为 1发送 RST 报文给客户端拒绝连接。 另外 服务端的监听队列有两种 TCP 半连接队列和全连接队列是服务端在处理 TCP 连接时维护的两个队列它们的含义如下 半连接队列也称** SYN 队列**是存放已收到客户端的 SYN 报文但还未收到客户端的 ACK 报文的连接请求的队列。服务端会向客户端发送 SYNACK 报文并等待客户端的回复。全连接队列也称** accept 队列**是存放已完成三次握手但还未被应用程序 accept 的连接请求的队列。服务端会从半连接队列中移除连接请求并创建一个新的 socket然后将其放入全连接队列。 半连接队列和全连接队列都有最大长度限制如果超过限制服务端会根据 tcp_abort_on_overflow 参数的值来决定是丢弃新的连接请求还是发送 RST 报文给客户端。 它们和 socket 的关系是 服务端通过 socket 函数创建一个监听 socket并通过 bind 函数绑定一个地址和端口然后通过 listen 函数指定监听队列的大小。当客户端发起连接请求时服务端会根据 TCP 三次握手的进度将连接请求放入半连接队列或全连接队列。当应用程序调用 accept 函数时服务端会从全连接队列中取出一个连接请求并返回一个新的 socket 给应用程序用于和客户端通信。 再次理解“三次握手” 在前面几个小节中我们知道了什么是连接也了解了 TCP 的三次握手过程和 TCP 状态的变化。在了解这些前提后我们再来谈谈 TCP 为什么是三次握手。 TCP 连接除了要保证建立连接的效率、验证全双工之外虽然它不保证 100%的可靠性但是它是用于保证可靠性和流量控制维护的某些状态信息包括 Socket、序列号和窗口大小的前提。 那么问题就转化为为什么只有三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接 结论 阻止重复历史连接的初始化主要同步双方的初始序列号避免资源浪费 阻止重复历史连接的初始化 三次握手的首要原因是防止旧的重复连接初始化造成混乱。 首先谈谈什么是『历史连接』。 有这样一个场景假如客户端先发送了 SYN 报文Seq90然后它突然关机了好巧不巧SYNSeq90也被网络阻塞了导致服务端并未收到。当客户端重启后又向服务端发送了 SYN 报文Seq100以重新发起连接。这里的 SYNSeq90就被称为历史连接。 注意这里的 SYN 不是后面要讲的『重传』SYN因为序列号不同。 TCP 的三次握手通过序列号和确认号的机制来防止旧的重复连接初始化造成混乱。具体来说 在第一次握手中客户端发送一个 SYN 报文携带一个随机的初始序列 Seqx表示客户端想要建立连接并告诉服务端自己的序列号。在第二次握手中服务端回复一个 SYNACK 报文携带一个随机的初始序列号 Seqy表示服务端同意建立连接并告诉客户端自己的序列号。同时服务端也确认了客户端的序列号将确认号 ack 设置为 x1表示期待收到客户端下一个字节的序列号。在第三次握手中客户端回复一个 ACK 报文将确认号 ack 设置为 y1表示确认了服务端的序列号并期待收到服务端下一个字节的序列号。至此双方都同步了各自的初始序列号并确认了对方的初始序列号连接建立成功。 这样的过程可以防止旧的重复连接初始化造成混乱因为 第一次握手如果客户端发送的 SYN 报文是旧的重复报文那么它携带的初始序列号 Seqx 可能已经被服务端使用过或者超出了服务端期待的范围。这样服务端收到这个旧的 SYN 报文后会认为它是无效的或者已经过期的不会回复 SYNACK 报文也不会建立连接。第二次握手如果服务端回复的 SYNACK 报文是旧的重复报文那么它携带的初始序列号 Seqy 可能已经被客户端使用过或者超出了客户端期待的范围。这样客户端收到这个 SYNACK 报文后会认为它是无效的或者已经过期的不会回复 ACK 报文也不会建立连接。第三次握手如果客户端回复的 ACK 报文是旧的重复报文那么它携带的确认号 ack 可能已经被服务端使用过或者超出了服务端期待的范围。这样服务端收到这个 ACK 报文后会认为它是无效的或者已经过期的不会分配资源给这个连接也不会进行数据传输。 代入上面假设的场景如果在 SYNSeq100正在发送的途中原先 SYNSeq90刚好被服务端接收那么服务端会返回 ACKSeq91客户端应该收到的是 ACKSeq101而不是 ACKSeq91此时客户端就会发起 RST 报文以终止连接。服务端收到后释放连接。 经过一段之间后新的 SYNSeq100被服务端接收服务端返回 ACKSeq101客户端检查确认应答号是正确的就会发送自己的 ACK 报文连接成功且避免了旧的重复连接初始化造成混乱。 因此通过序列号和确认号的机制TCP 可以在三次握手中验证双方是否是当前有效的连接请求并且同步双方的初始序列号。这样可以防止旧的重复连接初始化造成混乱。 上面的例子是服务端先收到了『旧 SYN』报文的情况如果服务端先收到了『新 SYN』报文再收到『旧 SYN』报文时会发生什么 从数据结构的角度理解这个过程如果服务端在收到 RST 报文之前先收到了「新 SYN 报文」那么服务端会认为客户端想要建立一个新的连接而不是继续之前的连接。服务端会为新的 SYN 报文分配一个新的 TCB并发送 SYNACK 报文给客户端。同时服务端会保留旧的 TCB直到收到 RST 报文或者超时。这样服务端就可以同时处理两个不同的连接请求而不会混淆它们。 为什么两次握手不能防止旧的重复连接初始化造成混乱呢 如果只有两次握手那么客户端发送的 SYN 报文可能会在网络中延迟导致服务端收到一个过期的连接请求从而建立一个无效的连接浪费资源。 这是因为在『两次握手』的情况下服务端只要收到了客户端发送的第一个报文就认为它已经建立好了这个方向的连接立即处于 ESTABLISHED 状态。然而客户端只有当收到服务端发送的 ACKSYN 报文后才会认为它处于 ESTABLISHED 状态。 问题就在于客户端和服务端切换到 ESTABLISHED 状态的时机不论多少次握手都会有时差这是由机制本身决定的。如果在『服务端处于 ESTABLISHED 状态客户端处于 SYN_SENT 状态并将要切换到 ESTABLISHED 状态之前』这个时间段内报文的传输出现了问题那么整个连接就会失败。 在这个时间段内如果客户端发送的旧 SYNSeq100较新 SYNSeq200更先被服务端收到服务端进入 ESTABLISHED 状态像客户端发送 SYNACKSeq101报文。客户端通过校验发现ACKSeq101不是自己期望的 ACKSeq201于是向服务端发送 RST 报文以终止连接。 直到新 SYNSeq200被服务端接收到以后才能正常建立连接。 但是这个过程中注意在两次握手的情况下服务端已经和客户端的建立了一个旧连接这个旧连接因为双方的确认应答序号不一致而被迫终止造成的后果不仅是终止了这个连接更在于白白浪费了建立连接和发送数据的资源图中 RST 之前我们知道建立连接是有成本的。 三次握手可以保证客户端在收到服务端的 SYNACK 报文后才确认连接如果客户端没有回复 ACK 报文那么服务端会认为连接请求无效不会建立连接。简单地说两次握手只能 100%地建立一个方向的通信信道客户端-服务端但是三次握手就能建立双方向的通信信道。 到底该如何理解呢 你发现了吗不论是上面分析三次握手还是两次握手最后一次总是单方面的报文TCP 协议是无法 100%保证这最后一个报文能被对方收到的那么分析问题时就把最后一次当做不存在。那么问题就变得简单了既然 TCP 是全双工的那么就要建立双方向的通信信道。两次握手中只有一次握手能 100%建立通信信道只有一个方向不满足 TCP 的全双工通信要求当然不行了。 双方向具体如何理解 我们知道只有处于 ESTABLISHED 状态的一端才能发送数据例如第一次握手后服务端处于 ESTABLISHED 状态那么意味着客户端-服务端这个方向的通信信道连接成功而不是指发送 SYN 这个方向图中的箭头。 问为啥这么确定地说 100% 因为没有第一次握手就没有第二次握手。 同步双方初始序列号 序列号是 TCP 协议实现可靠传输的一个重要机制它可以帮助双方识别和处理重复、丢失、乱序、延迟的数据包。 初始序列号是建立 TCP 连接时双方协商的一个随机数它可以防止历史连接的干扰和恶意攻击。 通过三次握手双方可以互相确认对方的初始序列号并在此基础上递增序列号来发送后续的数据包。这样一来一回才能确保双方的初始序列号能被可靠的同步。 避免资源浪费 刚才在介绍两次握手时说明了两次握手只能确保建立单方向的通信信道客户端-服务端这个过程对客户端是无感知的只要它没有收到第二次握手服务端发送的 SYNACK 报文就会根据超时重传机制发送若干 SYN 报文以请求连接。 例如如果客户端发送的 SYN 报文在网络中阻塞了重复发送多次 SYN 报文那么服务端在收到请求后就会建立多个冗余的无效链接造成不必要的资源浪费。即两次握手会造成消息滞留情况下服务端重复接受无用的连接请求 SYN 报文而造成重复分配资源。 两次握手不能根据上下文 SYN 的序列号来丢弃历史请求报文吗 两次握手只能在客户端端阻止历史连接而不能在服务端阻止历史连接。因为 两次握手可以根据 SYN 的序列号来丢弃历史报文但是不能阻止历史连接。也就是说如果客户端收到了一个过期的 SYNACK 报文比如之前网络延迟导致的它可以根据序列号判断这是一个历史连接并发送 RST 报文来拒绝连接。但是服务端在收到客户端的 SYN 报文后就进入了 ESTABLISHED 状态并没有『中间状态』来阻止历史连接。也就是说如果服务端收到了一个过期的 SYN 报文比如之前网络延迟导致的它无法根据序列号判断这是一个历史连接并可能建立一个无效的连接并向客户端发送数据。 3.3 重传机制 在上面的示例中我们知道客户端在发送数据后的一段时间内如果得不到服务端的回应会重新发送请求连接的报文这个过程通过『重发机制』完成重发机制根据不同因素的驱动主要分为两种 超时重传机制以固定时间为驱动。如上例。快速重传机制以的数据为驱动。 超时重传机制 对于报文的发送方如果收不到对方的应答有两种情况如下图 报文被对方丢弃了报文被对方收到了但是对方发出的确认应答丢包了 这是无法被发送方确定的即使确定了也没有意义。但是也不能让发送方傻乎乎地一直等这个应答所以设置了一个有效时间一旦报文发出没有在规定时间内收到对方发送的确认应答那么发送方会重新发送一份完全相同的报文。这就是 TCP 的超时重传机制。 重发超时的具体时间长度又是如何确定的呢 最理想的是找到一个最小时间它能保证“确认应答一定能在这个时间内返回”。然而这个时间长短随着数据包途径的网络环境的不同而有所变化。 TCP 要求不论处在何种网络环境下都要提供高性能通信并且无论网络拥堵情况发生何种变化都必须保持这一特性。 为此它在每次发包时都会计算往返时间Round Trip Time 也叫 RTT是指报文段的往返时间 及其偏差RTT 时间波动的值、方差。有时也叫抖动 。将这个往返时间和偏差相加重发超时的时间就是比这个总和要稍大一点的值即 RTORetransmission Timeout 超时重传时间。 RTT 的偏差也叫绝对误差是指 RTT 的真实值和平滑估计值之间的差值它反映了 RTT 的波动程度。将 RTT 的平滑估计值和偏差相加再乘以一个系数就可以得到 RTO 的值。一般来说这个系数是 4也就是说 RTO (SRTT RTTVAR) * 4其中 SRTT 是 RTT 的平滑估计值RTTVAR 是 RTT 的偏差。 那么 RTT 具体指的是什么呢 RTT 指的是数据发送时刻到接收到确认的时刻的差值也就是包的往返时间。 注意数据也不会被无限、反复地重发。达到一定重发次数之后如果仍没有任何确认应答返回就会判断为网络或对端主机发生了异常强制关闭连接。并且通知应用通信异常强行终止。 为什么 RTO (SRTT RTTVAR) * 4为什么超时重传时间 RTO 的值应该略大于报文往返 RTT 的值呢 超时时间不能太短也不能太长这是一个由大量测试和实践得出的经验公式具体因版本而异。使 RTO 比 RTT 的值稍大是为了避免因为网络延迟而导致的误判和不必要的重传因为重传会增加网络的负担和拥塞 。 关于这个经验公式的推导可以参看 郑烇老师讲的课 和《TCP/IP 详解 卷 1 协议》第 464 页。 举两个极端的例子注意看箭头和括号 超时时间 RTO 太大重发报文的间隔太长导致效率低下。超时时间 RTO 太小重发报文的间隔太小可能报文并未丢包就向网络中重发了报文因为网络传输有距离会增加网络拥塞的程度。最终出现雪崩效应让网络状况雪上加霜。 由此可见RTO 的大小取决于网络环境它会随时间而改变TCP 必须跟踪这些变化并实时做出调整以维持较好的性能。 略大于 RTT 的 RTO是上述两种情况的折中 注意 尽管经过了一系列实践和测试但事实上总会存在某些超时重传解决不了的情况即超时重传的 RTO 宁愿长也不能短缺点的严重性取决于具体场景。例如像多人网游这样对延迟要求十分高的场景使用超时重传就会很低效这就需要使用『快速重传』机制解决。 快速重传 快速重传有两种方式一种是基于重复 ACK 的快速重传另一种是基于 SACK 的快速重传 基于重复 ACK 的快速重传是指当发送方连续收到三个相同的 ACK 报文时就认为该序号对应的数据包丢失了于是在超时定时器到期之前就立即重传该数据包。基于 SACK 的快速重传是指当接收方收到失序的数据包时会在 TCP 头部增加一个 SACK 字段告诉发送方已经收到的数据包序号范围这样发送方可以准确地知道哪些数据包丢失了并且只重传丢失的数据包。 假设有这样的场景在超时重传的计时器还未触发这个时间段内客户端已经接收到服务端发送的若干相同 ACK 报文那么此时也就没有必要再等下去了毕竟服务端都已经收到了上次发送的报文直接重发丢失的报文就好了。这就叫快速重传。 在上例中客户端发送的 2 号报文丢包服务端发送多个 ACKSeq2报文其中第一个报文表示服务端接收到了 1 号报文。后续 3/4/5 号报文被服务端接收到后校验错误总共向客户端发送了 3 个 ACKSeq2报文。 一旦客户端满足了这两个条件就能触发基于重复 ACK 快速重传机制 在这个过程中没有触发超时重传并且收到了 1ACKSeq2 3ACKSeq2 但是基于重复 ACK 快速重传机制在很多时候只能重传一个报文如果要重传多个那么既需要对对端也支持网络状况也要允许难免出现兼容性问题。 基于 SACK 的快速重传机制解决了应该要重传哪些报文这一问题。 首先介绍『SACK』是什么 SACK 是选择性确认Selective Acknowledgment的缩写是一种 TCP 的选项用于允许 TCP 单独确认非连续的数据段从而减少重传的数据量和提高传输效率。SACK 的工作原理是当接收方收到失序的数据段时会在 TCP 头部增加一个 SACK 字段告诉发送方已经收到的数据段序号范围这样发送方可以准确地知道哪些数据段丢失了并且只重传丢失的数据段。SACK 选项并不是强制的只有当双方都支持 SACK 时才会被使用Linux 2.4 后默认支持。TCP 连接建立时会在 TCP 头中协商 SACK 细节。此外D-SACKDuplicate SACK是一种扩展的 SACK用于告诉发送方有哪些数据段被重复接收了。 从缓冲区的角度理解重发机制 我们知道TCP 的发送端和接收端都有各自的收发缓冲区而 TCP 的接收端可以提供 SACK 功能以 TCP 头部基类的 ACK 号字段来描述其接收到的数据。这些 ACK 号是有实际意义的在上文提到过它可以视为一个字符数组的下标。那么某几段数据丢失实际上就是这个字符数组中产生了『空缺』。 空缺指的是 ACK 号与接收端接收缓冲区中的其他数据之间的间隔即图中右边白色的空缺。而 TCP 发送端的任务就是通过重传丢失的数据来填补接收端缓冲区的空缺。要求是保证不能重复地发送接收端已经收到的数据。那么此时 SACK 就能很好地发挥作用减少不必要的重传。 关于 SACK 的具体实现参看《TCP/IP 详解 卷 1 协议》第 478 页。 3.4 连接的断开 四次挥手 TCP 的四次挥手的过程是这样的 第一次挥手主动关闭方客户端或服务器上例是客户端发送一个 FIN 标志位为 1 的数据包表示要结束数据传输进入 FIN_WAIT_1 状态等待对方的确认。第二次挥手被动关闭方服务器或客户端收到 FIN 包后发送一个 ACK 标志位为 1 的数据包表示已经收到对方的结束请求进入 CLOSE_WAIT 状态但还可以继续发送数据。主动关闭方接收到** ACK** 数据包进入** FIN_WAIT_2 **状态。第三次挥手被动关闭方在发送完所有数据后再发送一个 FIN 标志位为 1 的数据包表示自己也要结束数据传输进入 LAST_ACK 状态等待对方的最后确认。第四次挥手主动关闭方收到 FIN 包后发送一个 ACK 标志位为 1 的数据包表示已经收到对方的结束请求进入 TIME_WAIT 状态等待** 2MSL **时间后确保对方收到确认然后关闭连接释放资源进入 **CLOSE **状态。 注意 四次挥手左-右和左-右两个方向上都各自有 FIN 请求关闭连接报文红色和一个 ACK 确认关闭连接报文蓝色。 **主动关闭连接的一方才有 TIME_WAIT **状态。 常见问题 FIN 和 ACK 我知道为什么要有两个 FIN_WAIT 状态呢 两个 FIN_WAIT 状态的区别是FIN_WAIT_1 状态表示主动关闭方客户端或服务器发送了 FIN 包等待被动关闭方服务器或客户端的 ACK 包。而 FIN_WAIT_2 状态表示主动关闭方收到了被动关闭方的 ACK 包等待被动关闭方的 FIN 包。 一般情况下FIN_WAIT_1 状态持续的时间很短因为被动关闭方会马上回复 ACK 包。但是如果被动关闭方没有及时回复 ACK 包或者网络链路出现故障导致主动关闭方收不到 ACK 包那么主动关闭方就会一直处于 FIN_WAIT_1 状态直到超时或者重传达到一定次数后放弃连接并进入 CLOSED 状态。 Linux 内核中有一个参数 net.ipv4.tcp_orphan_retries 用来控制在收不到 ACK 包的情况下主动关闭方在销毁连接前等待几轮 RTO 退避。 而 FIN_WAIT_2 状态持续的时间取决于被动关闭方是否还有数据要发送以及是否及时发送 FIN 包。如果被动关闭方及时发送 FIN 包那么主动关闭方就会回复 ACK 包并进入 TIME_WAIT 状态。如果被动关闭方没有及时发送 FIN 包那么主动关闭方就会一直处于 FIN_WAIT_2 状态直到超时或者收到重复的 FIN 包后进入 TIME_WAIT 状态。 另外内核中的参数 net.ipv4.tcp_fin_timeout 用来控制在收不到 FIN 包的情况下主动关闭方在超时前等待多长时间。 什么是 TIME_WAIT 状态 处于 TIME_WAIT 状态的一端说明 它正在等待一段时间以确保对方收到了最后一个 ACK 包或者处理可能出现的重复的 FIN 包。 也处于一个半关闭的状态即它已经发送了 FIN 包表示不再发送数据但是还可以接收对方的数据直到对方也发送了 FIN 包。 **TIME_WAIT **状态也称为 2MSL 等待状态在这个状态下TCP 将会等待两倍于 MSL最大段生存期的时间有时也被称为加倍等待。每个实现都必须为 MSL 设定一个数值它代表任何报文段在被丢弃前在网络中被允许存在的最长时间。 什么是半关闭 半关闭状态是一种单向关闭的状态它只关闭了某个方向的连接即数据传输。另一个方向的连接即数据接收还是保持打开的。半关闭状态的作用是让一方可以继续发送数据直到把所有数据都发送完毕再发送 FIN 包。这样可以避免数据的丢失或者重复发送。 因此TIME_WAIT 状态存在的目的有两个 可靠地实现 TCP 全双工连接的终止防止最后一个 ACK 丢失而导致对方无法正常关闭。允许老的重复报文段在网络中消逝防止新的连接收到旧的报文段而导致数据错乱。 但是**TIME_WAIT **状态的缺点是 它会占用端口资源如果有大量的 TIME_WAIT 状态存在可能会导致端口资源耗尽无法建立新的连接。 它会延长连接的释放时间如果有新的连接请求到来需要等待 TIME_WAIT 状态结束后才能使用相同的端口。 TIME_WAIT 状态的持续时间是 2 倍的 MSL报文最大生存时间通常为 2 分钟或 4 分钟。在这段时间内该连接占用的端口不能被再次使用。 为什么 TIME_WAIT 状态的持续时间是 2 倍的 MSL 为了可靠地实现 TCP 全双工连接的终止防止最后一个 ACK 丢失导致对方重发 FIN需要在收到 FIN 后等待一个 MSL 的时间以便重发 ACK。假如最后一个 ACK 丢失服务器会重发一个 FIN虽然此时客户端的进程终止了但 TCP 连接依然存在依然可以重发最后一个 ACK为了允许老的重复分节在网络中消逝防止新的连接被旧的分节干扰需要在发送 ACK 后等待一个 MSL 的时间以便新的连接不会使用相同的套接字对。 在 CentOS 7 中MSL 为 60s 服务器出现大量 CLOSE_WAIT 状态连接的原因有哪些 CLOSE_WAIT 状态表示一个 TCP 连接已经结束但是仍有一方在等待关闭连接的状态。这一方是被动关闭的一方也就是说它已经接收到了对方发送的 FIN 报文但是还没有发送自己的 FIN 报文。 当出现大量处于 CLOSE_WAIT 状态的连接时很大可能是由于没有关闭连接即『代码层面上』没有调用 close() 关闭 sockfd 文件描述符。也可能是由于响应太慢或者超时设置过小导致对方不耐烦直接 timeout而本地还在忙于耗时逻辑。还有一种可能是 BACKLOG 太大导致来不及消费的请求还在队列里就被对方关闭了。 服务器出现大量 TIME_WAIT 状态连接的原因有哪些 首先要知道TIME_WAIT 状态是主动关闭连接的一方才会出现的状态。服务器出现大量的 TIME_WAIT 状态的 TCP 连接就是说明服务器主动断开了很多 TCP 连接。 问题就转化为什么原因会导致服务端主动断开连接 HTTP 没有使用长连接。即服务器使用了短连接这意味着每次请求都需要建立一个新的 TCP 连接而且在响应完毕后服务端会主动关闭连接导致产生大量的 TIME_WAIT 状态的连接占用系统资源端口号CPU内存影响新连接的建立。HTTP 长连接超时。如果客户端在一段时间内没有发送新的请求服务端会认为客户端已经不需要继续使用该连接就会主动关闭连接以释放资源。这个超时时间可以由服务端配置。服务器收到了客户端异常或重复的 FIN 包导致进入 TIME_WAIT 状态等待对方的 ACK 包但是没有收到只能等待超时后关闭。HTTP 长连接的请求数量达到上限。如果一个连接上发起的请求数量超过了服务端设定的最大值服务端会主动关闭连接以防止客户端占用过多的资源。服务端设置了过长的 MSL报文最大生存时间导致 TIME_WAIT 状态持续时间过长无法及时回收资源。 什么是长连接/短连接 长连接和短连接是指在 TCP 协议中连接的建立和关闭的方式。简单来说 长连接客户端和服务器建立一次连接后可以连续发送多个数据包不会主动关闭连接除非出现异常或者双方协商关闭。长连接适合于操作频繁点对点的通信可以减少建立和关闭连接的开销提高网络效率。短连接客户端和服务器每次通信都要建立一个新的连接发送一个数据包后就关闭连接。短连接适合于并发量大请求频率低的通信可以节省服务器的资源防止过多的无效连接。 如何解决服务器出现大量 TIME_WAIT 状态的连接这一问题 保证客户端和服务端双方的 HTTP header 中有Connection: Keep-Alive选项以使用长连接使得连接状态能被保持一段时间减少 TIME_WAIT 状态的连接数量提高效率。HTTP 长连接可以在同一个 TCP 连接上接收和发送多个 HTTP 请求/应答从而避免连接建立和释放的开销。但是如果“杀鸡用牛刀”只有一个 HTTP 请求也用长连接那么长连接也会占用资源。所以服务端一般会设置一个 keepalive_timeout 参数一旦计时器超出了这个范围且无新请求就会让连接处于 TIME_WAIT 状态。设置 tcp_fin_timeoutTCP 的 FIN 等待超时时间即服务器在收到客户端的 FIN 包后进入 TIME_WAIT 状态的最长时间。如果在这个时间内没有收到客户端的 ACK 包服务器会关闭连接。这个参数可以减少 TIME_WAIT 状态的连接数量节省系统资源。keepalive_requests 参数被用定义一条 HTTP 长连接上最大能处理的请求数量当超过最大限制时就会主动关闭连接变成 TIME_WAIT 状态的连接。其默认值是 100 意味着每个 HTTP 长连接最多只能承载 100 次请求。如果 QPS 每秒请求数很高时超过 10000 个默认值会让服务端频繁地关闭连接出现大量 TIME_WAIT 状态的连接。解决办法是增大 keepalive_requests 参数的值。 [注] 如果之前有 Socket 编程经验的同学在测试时总会遇到这种情况以某个端口运行进程如果测试时用 Ctrl C 终止了服务端进程这相当于服务端主动关闭连接在 TIME_WAIT 期间再用同一个端口测试就出现绑定失败的错误。值得注意的是在刚开始 Socket 编程时一般实现的是短连接 如何解决这个问题TIME_WAIT 状态的连接导致这段时间内绑定端口失败 【端口复用】在 TCP 连接没有完全断开之前不允许重新监听这个做法是为了保证 TCP 连接的可靠性和安全性防止新的连接被旧的分节干扰。但是在一些情况下这么做是不合适的比如 服务器需要处理非常大量的客户端的连接每个连接的生存时间可能很短但是每秒都有很大数量的客户端来请求。这个时候如果由服务器端主动关闭连接例如关闭某些只连接不传输数据的客户端的连接就会产生大量 TIME_WAIT 连接导致服务器的端口不够用无法处理新的连接。服务器应用程序意外终止或重启导致服务器端主动关闭连接进入 TIME_WAIT 状态。这个时候如果服务器应用程序想要重新监听同样的端口就会失败。 还记得 2.2 中提到的『四元组』唯一确定一个 TCP 连接吗实际上源 IP 和源端口对于某个服务端而言是固定的那么如果新连接的目的 IP 和目的端口号和 TIME_WAIT 状态的连接占用的四元组重复了就会出现问题。 在这些情况下可以通过一些方法来解决 TIME_WAIT 状态的问题比如 【主要】使用setsockopt()设置 socket 文件描述符的选项 SO_REUSEADDR 为 1 来允许 TIME_WAIT 状态的 socket 被重用即允许创建端口号相同但是 IP 地址不同的多个 socket 文件描述符。缩短 TIME_WAIT 的持续时间等。 测试 下面用一个例子来测试当客户端主动关闭连接时会出现什么情况。 // Sock.hpp #pragma once#include iostream #include string #include cstring #include cerrno #include cassert #include unistd.h #include memory #include sys/types.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include ctype.hclass Sock { private:const static int gbacklog 20;public:Sock() {}int Socket(){int listensock socket(AF_INET, SOCK_STREAM, 0);if (listensock 0){exit(2);}return listensock;}void Bind(int sock, uint16_t port, std::string ip 0.0.0.0){struct sockaddr_in local;memset(local, 0, sizeof local);local.sin_family AF_INET;local.sin_port htons(port);inet_pton(AF_INET, ip.c_str(), local.sin_addr);if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) 0){exit(4);}}int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len sizeof(src);int servicesock accept(listensock, (struct sockaddr *)src, len);if (servicesock 0){return -1;}if(port) *port ntohs(src.sin_port);if(ip) *ip inet_ntoa(src.sin_addr);return servicesock;}bool Connect(int sock, const std::string server_ip, const uint16_t server_port){struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(server_port);server.sin_addr.s_addr inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)server, sizeof(server)) 0) return true;else return false;}~Sock() {} }; // main.cc #include Sock.hppint main() {Sock sock;int listensock sock.Socket();sock.Bind(listensock, 8080);sock.Listen(listensock);while(true){std::string clientip;uint16_t clientport;int sockfd sock.Accept(listensock, clientip, clientport);if(sockfd 0){std::cout [ clientip : clientport ]# sockfd std::endl;}} } 运行 通过指令 netstat 查看这个进程确实已经被运行起来了并且正处于监听状态。现在用另一个会话用 telnet 工具在本地进行测试 注意到此时这个连接处于 ESTABLISHED 状态表示连接创建成功。 telnet 相当于客户端那么下面这个客户端主动关闭连接会发生什么呢 注意由于我只有一台主机可以用来测试实际上如果用其他主机作为客户端连接到这个 8080 的监听端口的话再用这个命令查看相关信息IP 地址可能和服务器运营商提供的公网 IP 不同这是因为后者提供的是虚拟 IP。 注意到在服务器上这个连接的状态变化为了 CLOSE_WAIT。这是因为我们的代码中没有在关闭连接时关闭文件描述符造成了在这段时间内占用了这个文件描述符。如果你在短时间内重复连接的话会发现文件描述符会一直递增同时也会出现 CLOSE_WAIT 状态的连接 我们知道文件描述符是有上限的而且连接本身也会占用资源如果客户端主动关闭连接后服务端却没有关闭文件描述符最终会导致进程崩溃。 在服务端中增加关闭连接操作 #include Sock.hppint main() {// ...while(true){// ... sleep (10);close(sockfd);std::cout sockfd had closed std::endl;} }在 sleep 的 10s 内服务端连接处于正常连接状态 当服务端主动调用 close关闭连接时虽然四次挥手已经完成但是作为主动断开连接的一方要维持一段时间的 TIME_WAIT 状态。在这个状态下连接已经关闭但其地址信息 IP 和 PORT 依旧是被占用的。 值得注意的是作为服务器一旦启动后无特殊需求如维护是不会主动关闭连接的上面代码模拟的通常是服务端进程因为异常而终止的情况。 文件描述符的生命周期随进程不论服务端进程是正常退出还是异常退出只要服务端进程退出此时就应该立即重启服务器。但问题在于由于是服务端主动关闭请求此时服务器必然存在大量处于 TIME_WAIT 状态的连接而它们在一段时间内占用了 IP 和端口。如果是双 11 这样的场景发生这种是被称之为事故是要被定级的。 操作系统提供了 Listen 套接字的属性以供地址复用。这样服务器一旦挂掉重启后虽然存在大量处于 TIME_WAIT 状态的连接但是这个选项可以绕过 TIME_WAIT 限制直接复用原先使用的地址。 只需要在 Socket 初始化时设置选项 // Sock.hpp::Sock int Socket() {// ...int opt 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt));// ... }并且将刚才在 main.cc 中增加的代码删除方面手动终止和重启服务端进程。 建立一个连接并主动关闭服务端 重启服务端进程并尝试重新建立连接 即使此时这个 PORT 对应的连接处于 TIME_WAIT 状态由于设置了地址复用选项可以无视它的存在跳过这段占用时间。 3.5 流量控制 TCP 除了要保证连接的可靠性还要保证数据传输的效率。这是因为TCP 在每次发送数据时网络和机器本身的承载能力是动态的。提升效率的主要手段在于减少“发送-回应”的次数即增大回应的粒度以多条数据为一组作为一次回应的内容这一组数据在缓冲区中就叫做『滑动窗口』。 在上文提到过『缓冲区』现在我们就对它有了简单的认识收发缓冲区的『剩余空间』决定了收发的能力。 为什么要流量控制 由于缓冲区的大小是固定的剩余空间也是动态变化的所以进程的收发能力也是动态变化的。上一时刻可能最大能接收 1000 字节的数据下一时刻可能只能接收 10 字节的数据如果依然按照这样的速率发送接收端的接收缓冲区就会经常处于满的状态这就可能会造成丢包问题我们知道这可能会触发丢包重传等一系列连锁机制。因此发送方不能盲目地发送数据要考虑对端的接收能力。 滑动窗口 TCP 的流量控制主要通过滑动窗口机制来实现的。 滑动窗口是指在 TCP 连接的数据传输过程中两端系统使用的流量控制机制。滑动窗口由发送方和接收方各自维护一个窗口大小表示当前可以发送或接收的数据量。发送方的窗口大小取决于接收方的『窗口大小』字段即接收方告诉发送方自己还有多少空闲缓存可以接收数据。 接收方的窗口大小取决于自己的缓存大小和已经接收但未确认的数据量。当接收方收到数据后会返回一个确认报文并在报文中携带自己的通告窗口大小告诉发送方可以继续发送多少数据。当发送方收到确认报文后会根据通告窗口大小调整自己的窗口大小并向前滑动窗口即更新已经发送和确认的序号范围。 这样通过滑动窗口协议可以实现发送方根据接收方的处理能力来调节发送速度从而实现流量控制。 其中滑动窗口大小的动态更新过程 窗口大小越大数据包的往返时间越短网络的吞吐量越高。接收端一旦发现自己的缓冲区快满了就会将窗口大小设置成一个更小的值以让发送端更新。发送端会根据接收到的新窗口大小控制发送速率。如果接收端的缓冲区满后窗口大小会被更新为 0表示发送方不应该再短时间内发送数据了为了保证通信的持续性接收端会定期发送窗口探测的数据段给发送端以告知发送端自己窗口的最新大小。如果仍然为零发送端就会继续等待下一个持续计时器超时再次发送窗口探测报文直到接收端的窗口变为非零。 在窗口大小为 0 这种情况下发送端除了通过等待接收端定时发送报文以更新窗口大小之外还能主动发送不含数据的报文以询问接收端的窗口大小。 图片引用自《TCP/IP 详解 卷 1协议》第 498 页。 图中的 C 代指 ClientS 代指 Server。由于 TCP 是全双工的所以 Client 和 Server 都需要收发数据因此都需要以这种方式得知对方的接收能力。 网络的吞吐率为 X N E [ T ] X\frac {N} {\mathbb {E} [T]} XE[T]N​其中 X 是吞吐率N 是网络中的数据包数量$\mathbb {E} [T] $是数据包的平均往返时间。 当发送方第一次发送数据给接收方时怎么知道对方接受数据的能力 实际上当发送方第一次发送数据给接收方时它是通过 TCP 的三次握手过程来知道对方接收数据的能力的。具体来说发送方在第一次握手时会发送一个 SYN 报文其中包含了自己的初始序列号ISN和最大段大小MSS。接收方在第二次握手时会回复一个 SYNACK 报文其中包含了自己的 ISN 和 MSS以及一个『窗口』大小表示自己当前可以接收的数据量。发送方在第三次握手时会回复一个 ACK 报文确认接收到了对方的 SYNACK 报文。这样三次握手完成后双方就知道了彼此的序列号、段大小和窗口大小从而可以根据这些信息来调整自己的发送速度和接收能力。 『窗口大小』字段在报头中占 16 位也就是 2 16 − 1 65535 2^{16} - 165535 216−165535这意味着窗口大小最大是 65535字节吗 不一定。TCP 窗口大小字段本身是 16 位的所以最大值是 65535 字节。但是TCP 还支持一种叫做窗口缩放的选项它可以在 TCP 三次握手期间协商一个缩放因子用于将窗口大小乘以一个 2 的幂从而扩大窗口的范围。窗口缩放选项的值可以从 0 到 14所以最大的缩放因子是 2 14 16384 2^{14}16384 21416384这样最大的窗口大小就可以达到 65535 × 16384 1 65535\times 163841 65535×163841 GB。 当然这个值也受限于操作系统缓冲区的大小和网络状况的影响。 *滑动窗口的原理 上面介绍了滑动窗口的概念下面要介绍缓冲区是如何实现滑动窗口的。 由于 TCP 为了保证可靠性而付出了一定的代价所以需要通过多种方式保证其效率例如减少『发送-接收』的次数即将若干个数据打包为一组再发送这一组的大小由一个『窗口结构』维护因为这个数据包的大小因网络和应用程序实际情况而异因此它是动态变化的叫做『滑动窗口』。 为什么减少『发送-接收』的次数就能提高效率呢 数据在网络中往返的时间越长通信效率越低。窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。 从数据结构和缓冲区的角度理解滑动窗口是一个变化的数值表示当前可以发送或接收的数据量。滑动窗口的大小取决于操作系统缓冲区的大小和网络状况。滑动窗口可以用两个指针来表示一个指向缓冲区中『已发送或已接收的数据』的第一个字节另一个指向缓冲区中『未发送或未接收的数据』的第一个字节。『这两个指针之间的距离就是滑动窗口的大小』。当数据发送或接收时这两个指针会相应地移动从而实现窗口的滑动。 以 TCP 的『发送窗口』为例 其中在这个状态下 绿色已发送并收到 ACK 确认的数据。蓝色已发送但未收到 ACK 确认的数据。黄色未发送但总大小在接收方接收范围内。红色未发送但总大小不在接收方处理范围内。 窗口是红色方框中的部分它由两部分组成那么滑动窗口表示的是当前状态下可以发送或接收的数据的范围。如果发送方已经发送了一些数据但还没有收到接收方的确认那么这些数据仍然属于滑动窗口的一部分蓝色直到收到确认或超时重传。同样如果接收方已经接收了一些数据但还没有交给应用层处理那么这些数据也仍然属于滑动窗口的一部分直到被应用层读取或丢弃。 滑动窗口主要需要实现两方面 希望一次性能发送尽可能多的数据给对方蓝色区域。保证对方能够来得及接收由接收方发送的报文中的窗口大小字段决定。 值得注意的是 滑动窗口的范围是数据的字节序号不是下标。字节序号是 TCP 协议为每个字节分配的一个唯一的编号用于标识数据的顺序和位置。字节序号是 32 位的整数从 0 0 0~ 2 32 − 1 2^{32−1} 232−1循环变化。 窗口由两部分组成一部分是已经发送但未收到 ACK 确认的一部分是未发送的。对于前者我们理想地认为接收方 100%收到那么接收方的接收缓冲区在短时间内就被占用了蓝色这么大的空间剩下的空间才是真正可用的缓冲区大小我们把黄色部分称为『可用窗口大小』。 窗口的『滑动』和『可用窗口大小』的维护通过三个指针实现 SND.UNA指向的是已发送但未收到确认的第一个字节的序列号蓝色的起始位置。SND.NXT指向未发送但可发送范围的第一个字节的序列号黄色的起始位置。它的意义是指示发送方下一次要发送的数据的位置作用是维护蓝色区域。SND.WND表示发送/提供窗口的大小红色方框。 由图可以得到右边界指针 S N D . U N A S N D . W N D SND.UNASND.WND SND.UNASND.WND SND.NXT 和 SND.UNASND.WND右边界之间的差值表示『可用窗口大小』即发送方还可以发送多少数据而不需要等待接收方的确认 如果 SND.NXT 等于 SND.UNASND.WND那么表示可用窗口为 0发送方必须停止发送数据直到收到接收方的窗口更新。如果 SND.NXT 小于 SND.UNASND.WND那么表示可用窗口为正发送方可以继续发送数据直到达到窗口的右边界。 即 可用窗口大小 [ 红色方框 ] S N D . W N D − [ 蓝色区域 ] ( S N D . N X T − S N D . U N A ) 可用窗口大小 [红色方框]SND.WND -[蓝色区域](SND.NXT - SND.UNA) 可用窗口大小[红色方框]SND.WND−[蓝色区域](SND.NXT−SND.UNA) 随着时间的推移当接收到返回的数据 ACK滑动窗口也随之右移。窗口两端的相对运动使得窗口增大或减小 关闭即窗口左边界右移。当发送数据得到 ACK 确认时说明这个数据在『这一刻』已经被接收端的确认窗口会减小。打开即窗口右边界左移使得可发送数据量增大。当已确认数据得到处理接收端可用缓存变大窗口也随之变大。收缩即窗口右边界左移这意味着可以发送或接收的数据量减少了。当接收方的缓冲区被填满了或者网络状况变差了或者发送方收到了重复的确认或者其他原因导致窗口变小。窗口右边界左移会降低数据传输的效率可能导致拥塞或超时。 发送方为了维护滑动窗口需要开辟发送缓冲区以存储待发送和已发送但未确认的数据并根据接收方和网络状况动态调整缓冲区和窗口的大小。 当应用程序向 TCP 协议栈发起发送请求时数据先被放入发送缓冲区然后由 TCP 协议栈将缓冲区中的数据发送出去。它的具体作用是记录当前还有哪些数据没有收到 ACK 应答。只有收到了 ACK 应答的数据才能从缓冲区中取出删除表示已经被使用。 发送缓冲区的大小决定了发送方的发送窗口的大小而发送窗口的大小又决定了一次能够发送的数据的大小也就是飞行报文的大小。飞行报文是指已经发送出去但还没有收到确认应答的报文也就是蓝色区域。如果飞行报文的大小与带宽时延积相等那么就可以最大化地利用网络带宽。也就是说当窗口越大时网络的吞吐率越高。 滑动窗口的大小是这样变化的 对于蓝色区域的几个数据包可以无需等待任何 ACK直接就能发送。当收到第一个 ACK 报文时滑动窗口向后移动继续发送第下一个数据包以此类推。绿色区域逐渐变大红色区域逐渐减小。操作系统会根据缓冲区中的数据是否有对应的 ACK 应答决定它是否被移出缓冲区。 当然这只是窗口变化的其中一种情况因为滑动窗口的动态变化的。但引起窗口移动的条件是『已经发送但未接收到 ACK 应答』的数据收到了 ACK 应答。 实际上当发送方发送了一个数据段后就会启动一个定时器如果在定时器超时之前收到了接收方的 ACK 应答就表示该数据段已经成功传输那么发送方就会把窗口向右移动一个数据段的大小从而可以继续发送下一个数据段。如果在定时器超时之前没有收到 ACK 应答就表示该数据段可能丢失或者延迟了那么发送方就会重传该数据段并把窗口缩小一半从而减少网络拥塞。 可用窗口大小是指接收方通知发送方的当前可接收的数据量它反映了接收方的缓冲区空间和网络拥塞程度。可用窗口大小的意义在于它可以使 TCP 协议适应不同的网络环境和传输需求提高网络的吞吐率和效率。可用窗口大小可以通过 TCP 头部中的窗口字段来表示但是由于该字段只有 16 位最大只能表示 65535 字节所以当网络带宽较大时可能会限制 TCP 的性能。为了解决这个问题TCP 引入了窗口缩放选项 (RFC 1323) 它可以通过一个缩放因子来扩展窗口字段的表示范围最大可以达到 1 GB。 接收窗口和发送窗口的大小是相等的吗 窗口的移动是通过指针偏移量实现的可以认为是缓冲区的下标的运算。这么说基本上是正确的。当接收方收到数据发送 ACK 确认应答报文报文的大小就是这个偏移量。这么说也基本上是正确的但是要注意报文的大小不一定等于窗口的偏移量因为报文中还包含了其他信息比如序列号、确认号、校验和等。 除此之外通信双方在交换报文时也是存在时间差的比如当接收方的应用进程读取数据的速度非常快的话这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小是通过 TCP 报文中的 Windows Size 字段来告诉发送方。那么这个传输过程是存在时延的所以接收窗口和发送窗口是约等于的关系。 滑动窗口只能向右移动吗理解两个指针的含义 不是的发送方的滑动窗口可以向两个方向移动分别是向右和向左。向右移动表示发送方可以发送更多的数据向左移动表示发送方已经收到了一些数据的确认。 也可以不移动例如发送方发送数据接收方回复 ACK如果接收方的上层应用程序一直不取出数据那么它的接收缓冲区就会一直减小。此时即使当发送方一直发送数据窗口也不会向右移动。 窗口大小也可能为零就像上图中的情况维护窗口的两个指针重合了说明对方的接收缓冲区已满偏移量为 0。 当发送方收到接收方发来的确认应答时SND.UNA 会向右移动相应地发送窗口也会向右移动这称为窗口合拢。当接收方通告了一个更大的窗口大小时SND.WND 会增加相应地发送窗口也会向右移动这称为窗口张开。 窗口的移动和 ACK 有什么关系 接收方只能在接收窗口内接收数据并且要及时将数据传递给应用层以免缓存溢出。当接收方收到发送方发来的数据报文时就会根据序号和校验和来判断是否正确并根据累计确认或选择确认的原则回复 ACK 确认报文通知对方已经成功接收。如果接收方发现有序号不连续或重复的数据段就会暂时缓存它们并重复回复最后一个正确连续序号的 ACK 报文以便让发送方重传丢失或错误的数据段。当接收方将所有缓存中的数据段按序交付给应用层后就会移动接收窗口的左边界并向右滑动窗口准备接收后续数据。 例如在上面这个例子中发送方起初一次性发送了序号 4/5/6 这三个数据包但是只收到了来自接收方 4 和 5 的 ACK 确认应答序号 6 暂时没有收到。那么这个状态下窗口的右边界只能从 6 开始只有收到了对应 ACK 确认应答的数据包才能被滑出窗口外。 [ACK 的含义] 另外还记得 ACK 表示的是什么吗–对于接收到 ACK 的一方它代表这对方已经接收到 ACK 序号之前的数据那么 ACK 就是我下次要发送的下一个数据的序号。只要收到了 ACK就代表这个序号的数据包被接收到了没有的话就等下次重发。 [强调连续性] 在这个意义下假如在上例中对于这一组连续的报文接收方没有收到中间序号为 5 的数据包在发送方重传以后如果收到了接收方 5 之后的 ACK 应答也认为 5 号报文被对方接收但是如果没有收到连续报文中间的数据的 ACK 应答例如收到了 4 号和 6 号但是没有收到 5 号的 ACK那么发送方会认为对方只收到了 4 号 ACK。这么做的原因是方便稍后重传数据包使得窗口的左边界能够单调地向一个方向移动接收到 ACK 就移动把接收到 ACK 的序号滑出窗口没有接收到就不动。 言外之意数据发送是否被对方确认最终还是要看发送方也就是要确认两次如果不是『连续序号』ACK 的话发生缺失处后面的报文不论被接收方确认了多少在发送方这边看都是不算数的。 这样窗口的更新方式就比较统一了只要收到 ACK 应答序号是几就更新到几不用担心报文丢失或确认应答丢失根据序号的定义丢失的报文最终是不会发送方被确认的窗口也就不会越过这个序号。 由于窗口由一个环形数组维护因此它不会出现越界问题需要处理的是跨越起点的两部分。参考环形队列的解决办法从上面的例子不难体会到滑动窗口解决的是效率问题而重传机制保证了一定程度的可靠性。 3.6 拥塞控制 网络是一种共享资源在网络中每时每刻都有无数台机器在使用 TCP 协议进行通信对于通信的参与方它们对网络是无感知的。因为通信双方只会交换对方的接收能力只关心对方的状态。极端地说如果网络上大部分发送方都在重传数据那么网络将会越来越拥堵就像滚雪球一样更何况网络本身就可能处于阻塞状态。 『拥塞控制』就是控制发送方发送的数据的数量以避免它们造成或加剧网络拥堵。拥塞控制通过『拥塞窗口』来维护。 拥塞窗口 拥塞窗口和发送窗口的关系 拥塞窗口是发送方维护的一个状态变量它表示当前网络的拥塞程度也就是发送方可以在没有确认的情况下发送的数据量。 发送窗口是发送方根据拥塞窗口和接收方通告的接收窗口计算出来的一个变量它表示发送方在当前时刻可以发送的数据范围。 发送窗口的大小等于拥塞窗口和接收窗口对方接受能力中的较小值即 swnd min(cwnd, rwnd)。 发送窗口的大小决定了发送方的传输速率和网络的吞吐量因此发送方要根据网络反馈来调整拥塞窗口的大小以达到最优的传输效率。 值得注意的是即使是单台主机一次性向网络中发送大量数据也可能会引发网络拥塞的上限值。 拥塞窗口 cwnd 变化的规则 只要网络中没有出现拥塞cwnd 就会增大但网络中出现了拥塞cwnd 就减少 拥塞窗口如何得知网络的阻塞情况 发送方没有在规定时间内接收到 ACK 应答报文也就是**发生了超时重传就会认为网络出现了拥塞。**主要有以下几种方法 慢启动拥塞避免拥塞发生 超时重传快速重传快速恢复 慢启动 慢启动即在两端建立 TCP 连接或由超时重传导致的丢包后将拥塞窗口设为一个较小的值每收到一个 ACK 就增加一个 MSS使得拥塞窗口呈指数增长。 这么做的原因是引用自 [REC5681] 在传输初始阶段由于未知网络传输能力需要缓慢探测可用传输资源防止短时间内大量数据注入导致拥塞。慢启动算法正是针对这一问题而设计。在数据传输之初或者重传计时器检测到丢包后需要执行慢启动。 慢启动的规则是当发送方每收到一个 ACK拥塞窗口 cwnd 的大小就会加 1。 假设没有出现丢包情况且每个数据包都有相应的 ACK第一个数据段的 ACK 到达说明可发送一个新的数据段。每接收到一个『好的 ACK 响应』慢启动算法会以 min(N,SMSS) 来增加 cwnd 值。这里的 N 是指在未经确认的传输数据中能通过这一“好的 ACK”确认的字节数。所谓的“好的 ACK”是指新接收的 ACK 号大于之前收到的 ACK。 以下内容引用自《TCP/IP 详解 卷 1 协议》第 521 页。 因此在接收到一个数据段的 ACK 后通常 cwnd 值会增加到 2接着会发送两个数据段。如果成功收到相应的新的 ACKcwnd 会由 2 变 4由 4 变 8以此类推。一般情况下假设没有丢包且每个数据包都有相应 ACK在轮后 W 的值为$ W2^k 即 即 即klog_2W$需要 k 个 RTT 时间操作窗口才能达到 W 大小。这种增长看似很快以指数函数增长)但若与一开始就允许以最大可用速率即接收方通知窗口大小发送相比仍显缓慢。( W 不会超过 awnd) 如果假设某个 TCP 连接中接收方的通知窗口非常大比如说无穷大这时 cwnd 就是影响发送速率的主要因素设发送方有较大发送需求。如前所述cwnd 会随着 RTT 呈指数增长。因此最终 cwnd(W 也如此会增至很大大量数据包的发送将导致网络痪 (TCP 吞吐量与 W/RTT 成正比。当发生上述情况时cwnd 将大幅度减小减至原值一半。这是 TCP 由慢启动阶段至拥塞避免阶段的转折点与 cwnd 和『慢启动阈值』(slow start thresholdssthresh) 相关。 下图左描述了慢启动操作。数值部分以 RTT 为单位。假设该连接首先发送一个包图上部返回一个 ACK接着在第二个 RTT 时间里发送两个包会接收到两个 ACK。TCP 发送方每接收一个 ACK 就会执行一次 cwnd 的增长操作以此类推。 右图描述了 cwnd 随时间增长的指数函数。图中另一条曲线显示了每两个数据包收到一个 ACK 时 cwnd 的增长情况。通常在 ACK 延时情况下会采用这种方式这时的 cwnd 仍以指数增长只是增幅不是很大。正因 ACK 可能会延时到达所以一些 TCP 操作只在慢启动阶段完成后才返回 ACK。Linux 系统中这被称为快速确认快速 ACK 模式。 慢启动算法中的发包个数按指数增长那么它应该什么时候停下 通过参数『慢启动阈值』ssthresh控制 当 cwnd ssthresh 时使用慢启动算法。当 cwnd ssthresh 时使用『拥塞避免』算法。 拥塞避免 如上所述在连接建立之初以及由超时判定丢包发生的情况下需要执行慢启动操作。在慢启动阶段cwnd 会快速增长帮助确立一个慢启动值。一旦达到阈值就意味着可能有更多可用的传输资源。如果立即全部占用这些资源将会使共享路由器队列的其他连接出现严重的丢包和重传情况从而导致整个网络性能不稳定。 为了得到更多的传输资源而不致影响其他连接传输TCP 实现了拥塞避免算法。一旦确立慢启动闻值TCP 会进入『拥塞避免』阶段cwnd 每次的增长值近似于成功传输的数据段大小这种随时间线性增长方式与慢启动的指数增长相比缓慢许多。更准确地说每当收到一个 ACK 时cwnd 增加 1/cwnd。 例如假定 ssthresh 为 8当 8 个 ACK 应答确认到来时每个确认增加 1/88 个 ACK 确认 cwnd 一共增加 1于是这一次能够发送 9 个 MSS 大小的数据变成了线性增长。 实际上拥塞避免算法就是将原本慢启动算法的『指数增长』变成了近似『线性增长』仍然处于增长阶段但是增长速度缓慢了一些。 如果一直这样随它增长下去网络中会出现大量数据造成一定拥堵然后出现丢包这时就需要对丢失的数据包进行重传。 此时触发了重传机制后需要使用『拥塞发生』算法解决。 拥塞发生 当有大量的数据包经过重传发送到网络中时网络处于阻塞状态需要使用『拥塞发生』算法解决。根据造成拥塞的重传机制主要包括两种 解决由于超时重传导致的拥塞算法解决由于快速重传导致的拥塞算法 发生超时重传的拥塞发生算法 当发生了超时重传会触发对应的拥塞发生算法 ssthresh 设为 cwnd/2cwnd 重置为初始值一般为 10Linux。即 10 个 MSS。 在 Linux 下通过ssSocket Statistics命令查看 在 80s 末期的 4.2UNIX 版本的 TCP 版本中这个初始值是 1MSS直至 cwnd 增长为 ssthresh。 但是这种做法的缺点是对于有较高带宽和较长延迟的大 BDP 链路网络链路这么做会使得带宽利用率低下。因为 TCP 发送方经重新慢启动回归到的还是未丢包状态 (cwnd 启动初始值设置过小。 尽管如此这么做仍然是一种比较激进的策略毕竟对于通信参与方而言慢启动会『突然』减少数据流之前好不容易把速度提上来这一旦出发了超时重传速率又跟刚连接时一样了。用户会感受到网络卡顿。 为解决这一问题针对不同的丢包情况重新考虑是否需要重回慢启动状态。若是由重复 ACK 引起的丢包引发快速重传cwnd 值将被设为上一个 ssthresh而非先前的 1 SMSS。在大多数 TCP 版本中超时仍是引发慢启动的主要原因。这种方法使得 TCP 无须重新慢启动而只要把传输速率减半即可。 发生快速重传的拥塞发生算法 我们知道TCP 的快速重传是基于冗余 ACK 的重传机制即接收方在收到一个乱序的数据包后会立即返回对前一个正确收到的数据包的确认报文ACK如果发送方连续收到三个或以上相同的 ACK就认为对应序号的数据包丢失了。此时发送端就会快速地重传不必等待超时再重传。 从快速重传机制可以知道这种错误不会那么严重因此不必等待代价高昂的超时重传。 再进入快速恢复阶段 cwnd cwnd/2;ssthresh cwnd。即将拥塞窗口设为当前拥塞窗口的一半并每收到一个冗余 ACK 就增加一个 MSS直到收到新的 ACK 为止。进入『快速恢复』算法。 这种机制的优点是可以快速地检测和恢复丢失的数据包减少了等待时间和网络负载。 快速恢复 快速恢复通常与快速重传配合使用目的是在数据包丢失后快速恢复发送窗口的大小避免过度降低发送速率。 举个例子假设发送方发送了数据包 M1,M2,M3,M4,M5接收方收到了 M1,M2,M4,M5但没有收到 M3。按照快速重传的规则接收方会连续发送三个对 M2 的重复确认ACK让发送方知道 M3 丢失了并立即重传 M3。这时按照快速恢复的规则发送方会执行以下步骤 将 ssthresh 设置为当前拥塞窗口 cwnd 的一半并重传丢失的数据包 M3。将当前的 cwnd 设置为 ssthress 加上 3 个最大报文段大小MSS即 cwnd ssthresh 3*MSS。这是为了保持网络的利用率避免因为重传而减少发送新数据包的数量。每收到一个冗余 ACK对 M2 的重复确认就将 cwnd 加上一个 MSS并发送一个新的数据包如果有。这是为了利用冗余 ACK 来增加拥塞窗口使得发送方可以继续发送数据包而不是等待重传计时器到期。当接收方收到重传的数据包 M3 后会发送一个新的 ACK对 M5 的确认表示已经收到了所有的数据包。这时发送方会将 cwnd 设置为 ssthresh并退出快速恢复阶段进入拥塞避免阶段。 发送方为什么收到新的数据后将 cwnd 重新设置为原先的 ssthresh ? 这是为了避免拥塞窗口过大导致网络再次出现拥塞。因为在快速恢复阶段发送方的拥塞窗口是根据冗余 ACK 来增加的而不是根据网络的实际情况来调整的。所以当收到新的数据后发送方认为网络已经恢复正常就将拥塞窗口重新设置为原先的 ssthresh也就是丢包前的一半然后再按照拥塞避免算法来逐渐增加拥塞窗口。这样做可以保证发送方不会过分占用网络资源也可以适应网络的变化。 TCP 拥塞控制的变化过程如下 图片来源SlideToDoc 另一张图也可以总结 图片来源TCP 协议的拥塞控制 其中 指数增长。刚开始进行 TCP 通信时拥塞窗口的值为 1并不断按指数的方式进行增长。加法增大。拥塞避免当拥塞窗口由慢开始增长到 “ssthresh 的初始值”16 时不再翻倍增长而是每次增加 1此为拥塞避免的“加法增大”降低了拥塞窗口的增长速度。图中已弃用乘法减小。拥塞窗口在线性增长的过程中在增大到 24 时如果发生了网络拥塞此时慢启动的阈值将变为当前拥塞窗口的一半也就是 12并且拥塞窗口的值被重新设置为 1所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是 12。快恢复由图可以看出快恢复和快重传是紧密相连的在执行快重传结束时就执行了快恢复快恢复则是把 “ssthresh 的值” 设置为快重传最后一次执行值的一半然后通过拥塞控制的 “加法增大” 进行线性的增长降低了发送方发送的速率解决了拥塞问题。 参与通信的双方都会根据网络状况来进行这些操作以保证网络的通畅。值得注意的是对于每台主机而言拥塞窗口的大小不一定非要相同即使它们处于同一局域网这取决于它们的发送速率、网络延迟、丢包率等因素。因此在同一时刻有的主机发生了网络拥塞有的却没有。 拥塞控制算法的目的就是让每个主机根据自己的情况动态调整拥塞窗口以达到最优的网络性能。这也算是 TCP 想尽可能快地将数据传输给对方同时也要避免给网络造成太大压力的折中方案。这是因为一旦连接处于网络拥塞状态 前期要让网络缓一缓对应着指数增长的缓慢且少。中后期网络恢复有一定能力承载更大的流量但是此时正处于“指数爆炸时期”为了保证通信效率使用了线性增长。 TCP 比 UDP 多了这么多步骤效率还能比 UDP 高吗 TCP 和 UDP 的效率比较并不是一个简单的问题它取决于很多因素比如数据包的大小、网络的质量、应用的需求等。一般来说UDP 比 TCP 更快但也不是绝对的。下面是一些影响 TCP 和 UDP 效率的因素 TCP 和 UDP 的报头大小不同。TCP 的报头至少有 20 字节最多有 60 字节而 UDP 的报头只有 8 字节。这意味着 UDP 的开销更小占用的空间更少。TCP 和 UDP 的确认机制不同。TCP 是可靠的协议它需要在发送方和接收方之间进行握手、确认、重传等操作以保证数据包的完整性和顺序。而 UDP 是不可靠的协议它不需要进行任何确认只是尽力而为地发送数据包。这意味着 UDP 的处理更快但也可能导致数据包的丢失或乱序。TCP 和 UDP 的传输方式不同。TCP 是基于字节流的协议它会将应用层的数据分割成多个字节并按照顺序发送。而 UDP 是基于消息的协议它会将应用层的数据封装成一个个数据块并保留消息边界。这意味着 UDP 可以更好地适应不同大小的数据包而 TCP 可能需要缓存或填充数据以适应网络段。 综上所述UDP 在一些场景下比 TCP 更快比如 数据包较小不需要分片或重组。网络质量较好丢包率较低。应用对实时性要求较高对可靠性要求较低。 而 TCP 在一些场景下比 UDP 更快比如 数据包较大需要分片或重组。网络质量较差丢包率较高。应用对可靠性要求较高对实时性要求较低。 3.7 延迟应答 TCP 中的延迟应答是一种优化策略它的目的是为了减少网络上的小数据包提高网络利用率和传输效率。它的原理是接收方在收到数据包后并不立即发送确认应答而是等待一段时间让缓冲区中的数据被处理从而增大窗口大小使发送方可以发送更多的数据。 值得注意的是延迟应答的目的不是保证可靠性而是保证留有时间让接收缓冲区中的数据尽可能被上层应用程序取出这样 ACK 中的窗口大小就可以尽可能地大从而增大网络吞吐量提高数据的传输效率。 但是延迟应答也有一些缺点比如 延迟应答会增加数据包的往返时间RTT可能影响某些对时延敏感的应用。延迟应答会使发送方等待更长的时间才能得到确认可能影响拥塞控制和流量控制的效果。延迟应答会使接收方缓冲区占用更长的时间可能影响接收方的处理能力。 因此在某些情况下需要关闭或调整延迟应答的机制以适应不同的网络环境和应用需求。一般来说有以下几种方法可以解决或缓解延迟应答的问题 修改操作系统的参数比如在 Linux 中可以通过设置/proc/sys/net/ipv4/tcp_delack_min来调整最小延迟时间。修改协议层的参数比如在 TCP 中可以通过设置TCP_QUICKACK选项来强制发送确认应答。 不是所有的数据包都可以延迟应答这些限制是为了保证数据的可靠传输避免发送方等待太久或者重复发送数据 数量限制每隔一定数量的数据包就必须发送一个确认应答一般是两个。时间限制超过最大延迟时间就必须发送一个确认应答一般是 200 毫秒。状态限制如果接收方没有数据要发送就不能使用捎带应答只能单独发送确认应答。 3.8 捎带应答 捎带应答是在延迟应答的基础上进行的也就是说接收方在收到数据包后并不立即发送确认应答而是等待一段时间看是否有其他数据要发送。如果有就把确认应答和数据一起发送这就是捎带应答。如果没有就单独发送确认应答。 捎带应答的好处是可以减少网络上的小数据包和开销提高网络利用率和传输效率。因为如果每次发送一个确认应答或一个数据包都需要占用一个 TCP 包的报头空间这些报头空间会占用网络资源增加网络开销降低网络性能。而如果把确认应答和数据一起发送就可以节省一个 TCP 包的报头空间减少网络资源的消耗提高网络性能。 假设有两个主机 A 和 B它们之间使用 TCP 协议进行通信A 是发送方B 是接收方。假设每个数据包的大小是 1000 字节延迟应答的最大时间是 200 毫秒每隔两个数据包就必须发送一个确认应答。下面是一个可能的通信过程 A 向 B 发送第一个数据包编号为 1。B 收到第一个数据包但不立即发送确认应答而是等待一段时间看是否有其他数据要发送。A 向 B 发送第二个数据包编号为 2。B 收到第二个数据包由于已经达到了数量限制就必须发送一个确认应答。假设此时 B 有数据要发送给 A就把确认应答和数据一起发送这就是捎带应答。假设 B 要发送的数据包编号为 3那么它就会在这个数据包中附加一个确认应答编号为 2。A 收到捎带应答和数据包知道前两个数据包已经被 B 正确接收并处理 B 发来的数据包。A 向 B 发送第三个数据包编号为 4。B 收到第三个数据包但不立即发送确认应答而是等待一段时间看是否有其他数据要发送。A 向 B 发送第四个数据包编号为 5。B 收到第四个数据包由于已经达到了数量限制就必须发送一个确认应答。假设此时 B 没有数据要发送给 A就单独发送一个确认应答编号为 5。A 收到确认应答知道前四个数据包已经被 B 正确接收。 在这个过程中在第二次和第四次通信时B 都使用了捎带应答的机制在同一个 TCP 包中即发送了确认应答又发送了数据。这样做可以减少网络上的小数据包和开销并提高网络利用率和传输效率。 另外捎带应答在保证发送数据的效率之外由于捎带应答的报文携带了有效数据因此对方收到该报文后会对其进行响应当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了同时也能确保捎带的 ACK 应答也被对方可靠的收到了。 3.9 面向字节流 当创建一个 TCP 的 socket 时同时在内核中会创建一个发送缓冲区和一个接收缓冲区。 调用 write 函数就可以将数据写入发送缓冲区中但是如果发送缓冲区已满write 函数会阻塞直到有足够的空间可以写入数据。发送缓冲区当中的数据会由 TCP 自行进行发送但是发送的字节流的大小会根据窗口大小、拥塞控制、流量控制等因素来动态调整。如果发送的字节数太长TCP 会将其拆分成多个数据包发出。如果发送的字节数太短TCP 可能会先将其留在发送缓冲区当中等到合适的时机再进行发送。 接收数据的时候数据也是从网卡驱动程序到达内核的接收缓冲区可以通过调用 read 函数来读取接收缓冲区当中的数据。但是如果接收缓冲区为空read 函数会阻塞直到有数据到达。接收缓冲区当中的数据也是由 TCP 自行进行接收但是接收的字节流的大小会根据窗口大小、确认机制等因素来动态调整。而调用 read 函数读取接收缓冲区中的数据时也可以按任意字节数进行读取。 由于缓冲区的存在TCP 程序的读和写不需要一一匹配例如 写 100 个字节数据时可以调用一次 write 写 100 字节也可以调用 100 次 write每次写一个字节。读 100 个字节数据时也完全不需要考虑写的时候是怎么写的既可以一次 read100 个字节也可以一次 read 一个字节重复 100 次。 实际对于 TCP 来说它并不关心发送缓冲区当中的是什么数据在 TCP 看来这些只是一个个的字节数据并且给每个字节分配了一个序号并通过序号和确认号来保证字节流的顺序和完整性。它的任务就是将这些数据准确无误地发送到对方的接收缓冲区当中就行了而至于如何解释这些数据完全由上层应用来决定这就叫做面向字节流。而 OS 也是一样的它只关心缓冲区的剩余大小而不关心数据本身。 3.10 粘包问题 首先要明确 粘包问题中的 “包”指的是应用层的数据包。在 TCP 的协议头中没有如同 UDP 一样的 “报文长度” 这样的字段。站在传输层的角度TCP 是一个一个报文过来的按照序号排好序放在缓冲区中。站在应用层的角度看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。 导致粘包问题的因素是报文之间的边界不清晰。 粘包问题指的是发送方发送的多个数据包在接收方被合并为一个数据包的现象。这是因为 TCP 是面向字节流的协议它不关心数据的逻辑结构只负责将字节流按序和完整地传输给对方。TCP 在发送或接收数据时都会通过缓冲区来进行优化根据网络状况和窗口大小来动态调整发送或接收的字节流的大小。这样就可能导致发送方发送的多个数据包被拼接在一起或者一个数据包被拆分成多个部分。 解决办法 对于定长的包保证每次都按固定大小读取即可。对于变长的包可以在报头的位置约定一个包总长度的字段从而就知道了包的结束位置。比如 HTTP 报头当中就包含 Content-Length 属性表示正文的长度。对于变长的包还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的只要保证分隔符不和正文冲突即可。 UDP 没有粘包问题 这是因为 UDP 是面向报文的协议它将数据视为一个个独立的报文每个报文都有自己的边界和长度。UDP 在发送或接收数据时都是以报文为单位不会对报文进行拆分或合并。UDP 不保证报文的顺序和完整性只负责将报文原封不动地传输给对方。 UDP 要冗余一些信息是因为 UDP 没有可靠性保证它不会对丢失、重复、乱序的报文进行处理这些工作需要交给应用层来完成。所以 UDP 通常会在报文中添加一些额外的信息如序号、校验和、长度等来帮助应用层识别和处理异常的报文。 3.11 TCP 异常情况 这是一个宽泛的问题下面就 TCP 协议的工作原理和常见的故障场景来简要介绍一些可能的异常情况 TCP 连接建立过程中的异常。这些异常通常是由于网络不通、目标主机或端口不存在、服务端应用程序阻塞或崩溃等原因导致的。例如 客户端发送 SYN 包后没有收到服务端的 SYNACK 包可能是因为网络不通或者服务端没有监听该端口。客户端发送 SYN 包后收到服务端的 RST 包可能是因为服务端拒绝了连接请求或者服务端没有监听该端口。客户端发送 ACK 包后没有收到服务端的数据包可能是因为服务端应用程序被阻塞或崩溃了。 当一个进程退出时该进程曾经打开的文件描述符都会自动关闭因此当客户端进程退出时相当于自动调用了 close 函数关闭了对应的文件描述符此时双方操作系统在底层会正常完成四次挥手然后释放对应的连接资源。也就是说进程终止时会释放文件描述符TCP 底层仍然可以发送 FIN和进程正常退出没有区别。 TCP 连接断开过程中的异常。这些异常通常是由于网络不稳定、主机宕机、应用程序异常退出等原因导致的。例如 客户端或服务端发送 FIN 包后没有收到对方的 ACK 包可能是因为网络不稳定或者对方主机宕机了。客户端或服务端发送 FIN 包后收到对方的 RST 包可能是因为对方应用程序异常退出了。客户端或服务端发送 RST 包后没有收到对方的任何响应可能是因为对方已经关闭了连接或者主机宕机了。 当客户端正常访问服务器时如果将客户端主机重启此时建立好的连接会怎么样 当我们选择重启主机时操作系统会先杀掉所有进程然后再进行关机重启因此机器重启和进程终止的情况是一样的此时双方操作系统也会正常完成四次挥手然后释放对应的连接资源。 TCP 连接传输数据过程中的异常。这些异常通常是由于网络拥塞、数据丢失、数据乱序、数据重复、数据错误等原因导致的。例如 客户端或服务端发送数据包后没有收到对方的 ACK 包可能是因为网络拥塞或者数据丢失了。客户端或服务端收到对方的数据包后发现序号不连续可能是因为数据乱序了。客户端或服务端收到对方的数据包后发现序号重复可能是因为数据重复了。客户端或服务端收到对方的数据包后发现校验和错误可能是因为数据错误了。 当客户端正常访问服务器时如果将客户端突然掉线了此时建立好的连接会怎么样 当客户端掉线后服务器端在短时间内无法知道客户端掉线了因此在服务器端会维持与客户端建立的连接但这个连接也不会一直维持因为 TCP 是有保活策略的。 服务器会定期客户端客户端的存在状况检查对方是否在线如果连续多次都没有收到 ACK 应答此时服务器就会关闭这条连接。 此外客户端也可能会定期向服务器 “报平安”如果服务器长时间没有收到客户端的消息此时服务器也会将对应的连接关闭。 其中服务器定期询问客户端的存在状态的做法叫做基于保活定时器的一种心跳机制是由 TCP 实现的。此外应用层的某些协议也有一些类似的检测机制例如基于长连接的 HTTP也会定期检测对方的存在状态。 TCP 协议本身具有一定的容错和恢复能力可以通过超时重传、滑动窗口、流量控制、拥塞控制等机制来处理一些异常情况。但是有些异常情况需要应用层协议或者用户干预来解决。例如 如果 TCP 连接建立失败可以尝试重新建立连接或者检查网络和目标主机是否正常。如果 TCP 连接断开失败可以尝试关闭套接字或者检查网络和对方主机是否正常。如果 TCP 连接传输数据失败可以尝试重发数据或者检查网络和对方主机是否正常。 4. TCP 小结 小结 TCP 协议这么复杂就是因为 TCP 既要保证可靠性同时又尽可能的提高性能。 可靠性 检验和。序列号。确认应答。超时重传。连接管理。流量控制。拥塞控制。 提高性能 滑动窗口。快速重传。延迟应答。捎带应答。 需要注意的是TCP 的这些机制有些能够通过 TCP 报头体现出来的但还有一些是通过代码逻辑体现出来的。 TCP 定时器 此外TCP 当中还设置了各种定时器。 重传定时器为了控制丢失的报文段或丢弃的报文段也就是对报文段确认的等待时间。坚持定时器专门为对方零窗口通知而设立的也就是向对方发送窗口探测的时间间隔。保活定时器为了检查空闲连接的存在状态也就是向对方发送探查报文的时间间隔。TIME_WAIT 定时器双方在四次挥手后主动断开连接的一方需要等待的时长。 理解传输控制协议 TCP 的各种机制实际都没有谈及数据真正的发送这些都叫做传输数据的策略。TCP 协议是在网络数据传输当中做决策的它提供的是理论支持比如 TCP 要求当发出的报文在一段时间内收不到 ACK 应答就应该进行超时重传而数据真正的发送实际是由底层的 IP 和 MAC 帧完成的。 TCP 做决策和 IPMAC 做执行我们将它们统称为通信细节它们最终的目的就是为了将数据传输到对端主机。而传输数据的目的是什么则是由应用层决定的。因此应用层决定的是通信的意义而传输层及其往下的各层决定的是通信的方式。 Socket 编程相关问题 Accept accept 要不要参与三次握手的过程呢 accept() 不需要参与三次握手的过程。三次握手是 TCP 协议在内核层面完成的accept 只是在应用层面从完成队列中取出一个已经建立的连接并返回一个新的套接字。也就是说连接已经在内核中建立好了accept() 只是一个查询和返回的过程并不影响三次握手的逻辑。 如果不调用 accept()可以建立连接成功吗 如果不调用 accept连接仍然可以建立成功只是在应用层面无法获取到新的套接字。这时连接会一直处于完成队列中直到被取出或者超时。如果完成队列满了那么后续的连接请求就会被拒绝或者忽略。 这么说的话如果上层来不及调用 accept 函数而且对端还在短时间内发送了大量连接请求难道所有连接都应该事先建立好吗 不是TCP 协议为了防止这种情况提供了一个未完成队列用来存放已经收到 SYN 包但还没有收到 ACK 包的连接。这些连接还没有建立成功只是处于半连接状态。如果未完成队列也满了那么后续的连接请求就会被丢弃。所以TCP 协议并不会为每个连接请求都建立成功的连接而是有一定的限制和策略。 那么这对队列有什么要求 这需要了解 TCP 协议在内核层面维护的两个队列未完成队列和完成队列。未完成队列用于存放已经收到 SYN 包但还没有收到 ACK 包的连接也就是半连接状态。完成队列用于存放已经完成三次握手的连接也就是全连接状态。 我们可以把 TCP 服务器看作是餐厅把客户端看作是顾客把未完成队列看作是等候区把完成队列看作是就餐区。那么 当顾客来到餐厅时需要先在等候区排队等候区的大小由餐厅的规模决定如果等候区满了那么后来的顾客就无法进入只能等待或者离开。当等候区有空位时顾客可以进入等候区并向餐厅发出就餐请求这相当于发送 SYN 包。当餐厅收到就餐请求时会给顾客一个号码牌并告诉顾客稍后会有空位这相当于发送 SYNACK 包。当顾客收到号码牌时会给餐厅一个确认信号并等待被叫号这相当于发送 ACK 包。当就餐区有空位时餐厅会根据号码牌叫号并将顾客从等候区移到就餐区这相当于完成三次握手并将连接从未完成队列移到完成队列。当顾客在就餐区用完餐后会离开餐厅并释放空位这相当于断开连接并清空队列。 对这两个队列的要求主要是 队列的大小。队列的大小决定了 TCP 服务器能够处理的连接请求的数量如果队列满了那么后续的连接请求就会被拒绝或者丢弃。队列的大小可以通过一些内核参数或者应用层参数来设置。例如 未完成队列的大小由内核参数net.ipv4.tcp_max_syn_backlog设置。完成队列的大小由应用层参数listen函数中的backlog参数第二个和内核参数net.core.somaxconn共同决定取二者中较小的值。 队列的处理策略。队列的处理策略决定了 TCP 服务器在遇到异常情况时如何响应客户端。例如 如果未完成队列满了TCP 服务器可以选择是否启用syncookie机制来防止syn flood攻击。如果启用了syncookie机制那么 TCP 服务器会根据客户端的 SYN 包计算出一个特殊的序号并在收到客户端的 ACK 包时验证其合法性。如果不启用syncookie机制那么 TCP 服务器会丢弃新来的 SYN 包并等待客户端超时重传或者放弃。如果完成队列满了TCP 服务器可以选择是否启用tcp_abort_on_overflow参数来决定是否直接发送 RST 包给客户端。如果启用了该参数那么 TCP 服务器会直接发送 RST 包给客户端并关闭连接。如果不启用该参数那么 TCP 服务器会丢弃客户端发送的 ACK 包并等待客户端重传或者放弃。 Listen listen 函数的第二个参数也就是 backlog 参数是用来设置完成队列的大小的。它表示餐厅可以同时容纳多少个就餐的顾客。如果 backlog 参数设置得太小那么餐厅就会很快满座无法接待更多的顾客。如果 backlog 参数设置得太大那么餐厅就会浪费空间和资源而且可能超过餐厅的实际规模。所以backlog 参数需要根据餐厅的服务能力和顾客的需求来合理设置。 参考资料 《图解 TCP/IP》《TCP/IP 详解 卷 1 协议》小林 coding本文许多图片都引用自这个博客特此声明。图文并茂强烈推荐结合第二本书一起学习。
http://www.ho-use.cn/article/10811972.html

相关文章:

  • 智能科技网站模板下载网站名和域名
  • 有网站加金币的做弊器吗6银行营销活动方案
  • 广州住建厅官方网站德州市建设小学网站
  • 详情页制作漳州seo网站快速排名
  • 眉山建网站wordpress会员是主机么
  • 百度做的网站迁移宝安网站设计哪家最好
  • 商务局网站群建设方案大型网站要多少钱
  • 网站首页缩略图 seo北京网页制作方案
  • wordpress怎样修改备案号那种登录才能查看的网站怎么做优化
  • 玮科网站建设推广链接赚钱
  • 海口市网站开发丽水网站建设报价
  • 网站建设制作要学什么普陀网站建设哪家便宜
  • 免费设计网站平台网站做代理商
  • 怎样收录网站网站做什么内容
  • 茂名专业做网站公司商派商城网站建设方案
  • 哪些群体对网站开发有需求辽宁智能建站系统价格
  • 知春路网站建设公司修改wordpress用户名密码忘记
  • 网站服务器基本要素百度企业推广怎么收费
  • 汕头网站开发武山县建设局网站
  • wordpress建两个网站响应式布局网站模板
  • 给网站做游戏视频怎么赚钱兼职招聘信息最新招聘
  • 给别人做设计的网站wordpress参数
  • 域名注册1元怎么建设seo自己网站
  • 中国建设行业峰会网站新赣州房产网
  • 山东中恒建设集团网站做电影网站都需要什么手续
  • 编辑网站设计培训班学费一般多少
  • 网站建设行业的趋势郑州网站权重
  • 广东网站备案电话号码哪里可以学做网站
  • 用wordpress建一个网站怎么出售友情链接
  • 片网站无法显示app开发的基本步骤