欢迎来到啊妮莫的学习小屋!
本篇文章将从TCP协议的概念到TCP协议的细节原理从浅入深, 带领你一起探究TCP协议的全貌. 文章中标记✨的部分为重点内容, 也是面试中经常拷打的部分; 话不多说, 让我们一起开始学习吧! O(∩_∩)O
目录
什么是TCP协议
TCP协议特点✨
TCP报文格式
三次握手和四次挥手✨
可靠性
效率性
基于字节流✨
基于TCP的应用层协议
什么是TCP协议
TCP(传输控制协议, Transmission Control Protocol) 是一种面向连接的, 可靠的, 基于字节流的传输层通信协议, 广泛应用于互联网中. 它旨在提供可靠的端到端通信; 在双方进行通信之前, 必须先通过三次握手建立连接. TCP同时提供了一系列的机制来保证数据的可靠传输, 这些机制包括: 序列号, 确认应答, 重传控制, 流量控制和拥塞控制.
TCP协议特点✨
-
基于连接
-
可靠传输
-
面向字节流
TCP报文格式
-
源端口, 目的端口: 占16位, 分别用于标识发送方和接收方的应用层服务. TCP协议是基于IP协议的基础上传输的, TCP报文中的源端口+IP报文中的源IP, 与目的端口+目的IP 一起, 组合起来可以唯一确定一条TCP连接
-
序号(Sequence Number):占32位, 在发送端发出的字节流中, 传输报文中的数据部分的每一个字节都有它的编号.
序号(Sequence Number)的语义与SYN控制标志位的值有关. 根据SYN的值是否为1, 序号表达不同的含义:
-
SYN=1时: 当前为三次握手建立连接阶段, 此时的序号为初始序号ISN(Initial Sequence Number), 通过算法来随机生成ISN.
-
SYN=0时: 数据传输阶段, 第一个报文的序号为ISN+1, 后面的报文序号, 为前一个报文的序号值+TCP数据大小(不包含TCP首部).
-
-
确认序号(Acknowledgement Number): 占32位, 如果设置了ACK控制标志位, 确认序号的值就是接收方期望下一接收到的数据包的序列号: 假设主机A已经收到了来自主机B序号为0~133的所有数据, 则主机A将要发送给主机B的报文中确认序号的值为134.
-
报文首部长度: 占4位, 该字段以4字节为单位, 也就是最大值为60字节. 由图可知: 默认最小报头长度为20字节(选项字段为0), 则报文首部字段默认为0101.
-
标志位共有六个比特位, 每个比特位对于一个标志位: URG, ACK, PSH, RST, SYN, FIN. 这些标志用于控制TCP的不同行为, 例如: 建立连接(SYN), 确认应答(ACK), 指示数据急迫性(URG) 等.
-
URG: 紧急标志, 为1表示当前报文字段中存在被发送端上层置为"紧急"的数据. 接收方应当优先处理这些数据. 紧急指针(16位)字段 指出了这部分紧急数据的结束位置.
-
ACK: 确认应答, 为1标识确认序号(32位)字段有效, 该报文段中包含对方已经成功接收报文段的确认.
-
PSH: 为1指示接收方应该尽快将数据交付给应用层
-
RST: 为1表示连接出现错误, 要求数据报接收方终止连接, 并重新建立连接.
-
SYN: 用于双方建立连接. TCP三次握手中的前两次中SYN=1.
-
FIN: 发送方已经没有需要发送的数据, 想要断开连接.
-
-
窗口大小(Window Size): 占16位, 告诉发送方当前接收方剩余缓冲区的大小, 以避免接收方缓冲区溢出, 数据丢包.
-
校验和(Checksum): 占16位, 用于检查TCP报文段的错误.
-
紧急指针(Urgent Pointer): 占16位, 当URG标志位为1时, 表明从当前序列号开始的紧急数据的字节偏移量.
ISN为什么要随机初始化✨
保证网络安全, 防止黑客攻击:三次握手的其中一个重要功能就是双方交换ISN(初始化序号), 以便让对方知道接下来该如何将接收到的数据按照序列号进行组装. 如果ISN是固定的, 黑客很容易获取到ISN, 并且伪造序列号进行攻击.
为什么要设计序号和确认序号两个字段?
-
TCP的数据传输是全双工的: 即在接收的时候也可以发送, 发送的时候也可以接收, 两者并不冲突.
-
在确认应答机制中,为了提高效率, 采用的捎带应答机制, 即在请求报文中携带ACK应答数据.
综上: 需要两个不同的字段来进行数据的传输
三次握手和四次挥手✨
接下来, 通过男女朋友交往和分手的例子形象理解一下三次握手和四次挥手的过程.
三次握手:
张三-->小美: 你长得很漂亮, 做我女朋友吧!
小美-->张三: 其实我也很欣赏你, 我答应你!
张三-->小美:太棒了, 咱们一起去旅游吧!
之后俩人开始了亲密的交往~
四次挥手:
张三-->小美: 你太懒了, 我要和你分手!
小美-->张三: 好, 臭男人, 等我把我的东西收拾完!
小美-->张三: 臭男人, 我的东西收拾好了, 分手吧!
张三-->小美: 好的再见 (内心挣扎一段时间等待对方回心转意)
从此双方形同陌路人...
三次握手过程解析
刚开始Client处于Closed的状态, Server处于Listen状态.
-
第一次握手: Client给Server发送SYN报文, 其中包含Client的初始化序号ISN(C). Client进入
SYN_SEND
状态. -
第二次握手:Server收到Client的SYN报文后, 也会发送自己的SYN报文作为响应, 其中包含Server的初始化序号ISN(S). 并且Server会将Client发送的ISN(C)+1, 作为ACK发送给Client. Server进入
SYN_RCVD
(半连接)状态. -
第三次握手:Client收到Server的SYN报文后, 会将Server发送的ISN(S)+1, 作为ACK发送给Server, 此时Client处于
ESTABLISHED
状态. 当Server收到来自Client的ACK报文后, 也进入ESTABLISHED
状态. 双方建立连接成功.
为什么是三次握手,两次四次行吗?
三次握手是确认通信双方都具有发送和接收能力的最小握手次数.
第一次握手,当Server收到Client的SYN请求后, Server端可以确定: Server的接收能力正常, Client的发生能力正常.
第二次握手, Client收到来自Server的应答, Client可以确定: Client的发送和接收能力正常, Server的发送和接收能力正常. 不过因为此时Server并没有收到来自Client的ACK, 因此无法确定自己的发送能力和Client的接收能力是否正常.
第三次握手后, Server收到了ACK, 可以确认自己的发送能力以及对方的接收能力没问题. 此时双方都进入ESTABLISHED
状态.
两次握手可能引发的问题
-
Server端无法确认自己的发送能力以及Client端的接收能力是否正常
-
SYN-Flood攻击:
假设TCP是两次握手,客户端在短时间内不断的向服务端发起SYN请求, 使得服务端进入
ESTABLISH
状态,而客户端将服务端发送来的ACK报文直接丢弃, 这样双方建立连接的成本嫁接到了服务端: 服务端需要维护大量的连接, 而客户端却不用. 如此:在两次握手即可建立连接的情况下, 即使是一个单片机也可以造成服务器大量资源浪费.在三次握手的机制下, 攻击者也可能会发起SYN洪水攻击, 导致服务端生成大量半连接(SYN_RCVD),从而消耗系统资源, 目标系统运行缓慢,无法提供正常的服务. 但是客户端也需要维护一定数量的连接. 相对来说这样服务端不容易受到攻击.
什么是半连接队列?
半连接状态: 客户端向服务端发起SYN连接, 服务端进行了回应, 进入SYN_RCVD
状态, 此时服务端处于半连接状态.
服务器会把此种状态的请求连接放入一个队列中, 也就是半连接队列.
如果一直收不到客户端的ACK会发生什么?
每个半连接都设有一个定时器, 超过时间未收到ACK, 服务端会重传SYN-ACK消息, 重传次数达到系统规定的最大重传次数, 系统会将该半连接信息从队列中删除.
三次握手过程中可以携带数据吗?
第一次和第二次不可以携带数据.
防止黑客恶意攻击:黑客可以在首次的SYN报文中放入大量数据, 然后疯狂重复发SYN报文, 这样服务端需要花费很多时间, 内存空间来处理这些数据.
第三次握手可以携带数据: 客户端已经处于ESTABLISHED
状态, 并且客户端已经知道服务端的发送和接收能力正常.
四次挥手过程解析
-
第一次挥手:Client向Server发送FIN, Seq=K. 用于关闭从Client到Server的数据传送, Client进入
FIN_WAIT_1
状态. -
第二次挥手:Server收到FIN后, 发生ACK(K+1)作为应答, Server进入
CLOSE_WAIT
状态. -
第三次挥手:Server向Client发送FIN, Seq=L. 用于关闭从Server到Client的数据传送, Server进入
LAST_ACK
状态. -
第四次挥手:Client收到FIN后, 发送ACK(L+1)作为应答, 并进入
TIME_WAIT
状态, 等待2MSL(2倍的最大报文存活时长), 确保ACK被Server端接收到了, 后进入CLOSED
状态. 四次挥手结束.
为什么是四次挥手?
在三次握手时: 服务端的ACK和SYN可以一起发送; 因为ACK报文是用于应答的, SYN报文是用于同步的, 二者由内核直接发送.
在四次挥手时: FIN信号是由于应用层调用close所以才发送的, 而服务端在收到FIN报文后, 可能还有需要向客户端发送的报文, 所以只能先回复一个ACK报文, 告诉客户端"你发的报文我收到了". 当服务端报文发送完了, 才会发起FIN请求, 因此不能一起发送.
TIME_WAIT状态和CLOSE_WAIT状态
TIME_WAIT状态
-
主动关闭连接的一方在发送最后一个ACK报文之后,进入TIME_WAIT状态.
-
TIME_WAIT状态的持续时间为2MSL(2倍的最大报文存活时间).
-
在TIME_WAIT状态期间, 该连接处于等待状态, 以确保网络中的任何未完成或延迟的分节都能到达目的地(确保服务器已经成功收到并且处理了ACK报文).
-
在TIME_WAIT状态期间, 这个套接字不会分配给其他连接使用, 因此会占用一定的系统资源.
CLOSE_WAIT状态
-
被动关闭连接的一方在收到关闭连接请求并发送ACK报文后,进入CLOSE_WAIT状态
-
被动关闭连接一方如果没有发送FIN报文, 则CLOSE_WAIT状态会一直持续下去, 导致资源泄露.
为什么需要TIME_WAIT状态?
-
保证可靠的关闭连接:理论上, 四个报文发送完毕, 就可以直接进入
CLOSED
状态, 但是可能网络是不可靠的, 最后一个ACK报文可能丢失, 此时对端会重传FIN报文. 所以TIME_WAIT
状态是用来重发可能丢失的ACK报文的. -
避免连接复用问题:确保本次连接所产生的所有数据段从网络中消失, 防止新连接收到之前旧连接的延迟数据包, 避免数据包混乱.
为什么等待时间是2MSL?
客户端无法知道ACK是否被服务端成功接收, 因此需要等待; 假设ACK没有到达服务端, 服务端会超时重传FIN报文(timeout retransmit), 客户端收到新的FIN报文, 再次发送ACK. 这个等待时间至少是: 服务端的timeout+FIN送达客户端时间; 为了保证可靠性, 采用更加保守的等待时间2MSL.
客户端发送ACK, 等待ACK到达对端需要MSL, 等待对方的FIN报文到达也是MSL. 所以, 在2MSL时间内没有收到FIN, 说明服务端已经成功收到ACK.
TIME_WAIT状态引发的问题?
TIME_WAIT状态下的套接字暂时不会关闭, 端口依然被占用.
-
服务端短时间内关闭大量Client连接, 会导致服务端出现大量的TIME_WAIT连接, 严重消耗服务器资源.
-
客户端短时间内关闭大量连接, TIME_WAIT状态连接的端口依然被占用, 端口资源被耗尽, 则一段时间内无法发起新的连接
什么情况下会出现大量的CLOSE_WAIT状态?
服务端没有进行第三次挥手, 没有发送FIN结束报文. 也就是没有正确关闭连接所对应的文件描述符.
大量的CLOSE_WAIT状态会导致资源泄露, 可能到最后没有可分配的文件描述符, 从而使得服务崩溃.
可靠性
TCP协议提供可靠的端到端的数据传输通信; 那么哪些情况是不可靠的呢? 例如: 数据丢包, 乱序, 数据重复发送, 数据发送太快导致接收端来不及处理等等.
建立连接
通信双方通过三次握手建立连接, 可以预先确定对方的接收能力和发送能力没有问题.
序号机制
在TCP建立连接初期, 双方各自随机选择一个初始化序列号, 随后传输的每个报文的序号基于初始值递增, 增量为报文的数据字节大小, 这样可以确保数据的有序传输和重组.
假设主机A需要向主机B发送一个1000字节的数据流, 每次传输的报文段长度为100字节, 则数据流的首字节编号为0; 将该数据流构建出10个报文段, 第一个报文段的序号0, 第二个报文段的序号为100, 第三个200, 以此类推. 报文的序号被写入TCP报头中的32位序列号字段中.
确认应答
当对端收到了发送的消息报文后, 会发送过来ACK应答报文,ACK标志位为1, 确认序号的值为期待下次收到的报文序号. (PS: 为了提高通信效率, 一般会采用捎带应答,累计确认等机制)
超时重传
当发送方发送数据后, 如果在预定时间内未收到接收方的ACK确认报文, 发送方会假设该数据段丢失, 并且重新发送该数据段.假如一个数据段经过多次重发也没有收到对端的确认报文, 那么就会认为接收端异常, 强制关闭该连接.
数据校验
TCP报头包含校验和字段, 通过校验和的方式, 接收端可以检测出数据是否有差错和异常, 假如有差错就会直接丢弃TCP报文, 让发送端重新发送.
流量控制
当接收方来不及处理发送方的数据时, 可以通过滑动窗口机制, 告诉发送方降低发送速率, 防止数据包丢失.流量控制同时也可以提升TCP数据传输的效率, 因此在下文的效率篇会重点讲解.
拥塞控制✨
拥塞控制可以避免因为网络拥堵造成的丢包, 主要包括: 慢启动, 拥塞避免, 超时重传, 快速恢复四种策略.
在某段时间内, 若对网络中的某一资源的需求超过了该资源所能提供的可用部分, 网络性能就要变坏, 这种情况就叫做网络拥塞; 如果在网络状态已经比较拥堵的情况下, 贸然发送大量数据, 不仅仅会加重网络的负荷, 同时也会造成大量数据包丢失.
-
慢启动: 连接建立后将拥塞窗口的初始大小cwnd(congestion window)设置为一倍的MSS(Maximum Segment Size最大报文段长度), 即cwnd=1*MSS. 为了便于分析, 省略MSS, 称初始cwnd为1, 下文同理.
在慢启动阶段, 每收到一个ACK, cwnd翻倍, 这一阶段窗口大小呈指数增长.
-
拥塞避免: 当cwnd达到慢启动阈值ssthresh(Slow Start Threshold)时, 进入拥塞避免阶段. 这时每收到一个ACK, cwnd+1;
-
超时重传: 随着窗口的增长, 网络中传输的数据越来越多, 最终超出带宽限制, 出现丢包. 发送方会对数据进行超时重传, 将ssthresh调整为当前cwnd的一半, 然后将cwnd重置为1, 重新开始慢启动阶段.
-
快速恢复: 当发送方收到三个重复的ACK时, 判定相应的报文丢失, 立即重传, 将ssthresh调整为当前cwnd的一半, cwnd不必从1开始, 而是和新的ssthresh相等, 重新开始拥塞避免阶段.
拥塞窗口大小为什么先指数增加后线性增长?
窗口大小首先以指数递增用来探测网络的拥塞程度, 执行拥塞避免算法后, 拥塞窗口线性缓慢增大, 防止网络过早出现拥塞.
效率性
流量控制✨
接收端处理数据的速度是有限的, 如果发送端发的太快, 导致接收端的缓冲区被填满, 这个时候如果发送端继续发送数据, 就会造成数据丢失和资源浪费.
因此TCP支持根据[接收端]的处理能力, 来决定[发送端]的发送速度, 这个机制就叫做流量控制.
流量控制是通过滑动窗口机制来实现的.
发送方的滑动窗口
下图是发送方的缓冲区数据, 根据被处理的情况分为四个部分, 其中蓝色方框是发送窗口, 橙色方框是可用窗口.
当发送方将可发送数据全部发出去后, 可用窗口大小就变为0了, 在未收到ACK之前, 发送方无法继续发送数据了.
当发送方接收到之前发送数据22~24
字节的ACK报文后, 滑动窗口向右边移动3字节, 则后续就可以发送34~36
字节的数据.
接收方的滑动窗口
接收方的滑动窗口比较简单. 可以划分为3部分.
以下是流量控制的动态图
由拥塞控制以及流量控制可知: 发送方的发送窗口大小不仅仅取决与接收方的接收窗口大小, 也取决于网络的拥塞程度. 因此: 发送窗口的最大值=min(接收窗口, 拥塞窗口).( 也可以写作 swnd=min(rwnd,cwnd) ).
快速重传
当发送方连续收到3个重复的ACK时, 判定相应的报文丢失, 立即重传. 这样可以在不等待超时的情况下重传丢失的报文段, 避免了数据丢失时的长期等待, 提示传输效率.
累计确认
TCP的累计确认是指接收方发送的ACK报文中的确认号表示的是接收方期望接收到的下一个字节的序列号. 这意味着所有比这个确认号小的字节都已经被成功接收.
捎带应答
在TCP通信过程中, 数据发送方和接收方需要通过确认应答ACK来确保数据的可靠传输. 捎带应答技术允许在数据传输过程中, 将确认应答信息"捎带"在数据包中一起发送, 而不是单独发送一个ACK包.
延迟应答
接收方在接收到每个报文后, 不会立即发送ACK报文, 而是会等待一段时间.
-
等待网络中的其他报文段到达, 这样接收方可以将多个确认合并为一个ACK报文, 减少ACK报文的数量, 从而降低网络开销
-
等待应用进程读取数据, 使得滑动窗口变大, 并在下次的报文中告知数据发送方, 从而增大数据传输量, 提升传输效率.
基于字节流✨
TCP协议与UDP协议的区别之一就是: TCP将数据视为一连串的字节流, 不关心数据包的边界; 而UDP报文中有UDP报文长度字段, 因此可以确定其边界.
没有边界, 如何理解呢? 就是上层不知道这次读取上来的是几个TCP数据包, 可能是一个, 半个, 也可能是好几个.
而没有边界就会使数据包粘在一起,就像一个一个的粘包一样.
粘包问题
解决粘包问题的思路核心: 明确两个包之间的边界.
-
在接收端接收的时候采用定长的方式接收
-
数据包的尾部加上明确的分隔符
-
在报头加上数据包总长度字段
-
使用应用层协议对边界进行区分
基于TCP的应用层协议
协议 | 默认端口 |
---|---|
HTTP(超文本传输协议) | 80 |
FTP(文件传输协议) | 20用于传输数据, 21用于传输控制信息 |
SMTP(简单邮件传输协议) | 25 |
TELNET(网络电传) | 23 |
SSH(安全外壳协议) | 22 |
感谢铁汁萌花费时间来看我写的文章, 希望上面的内容可以对你有所帮助~