TCP协议各项机制详解

Posted Geek.Fan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP协议各项机制详解相关的知识,希望对你有一定的参考价值。

        TCP协议全称"传输控制协议(Transmission Control Protocol)", 人如其名, 要对数据的传输进行一个详细的控制。

        下来按照"安全"和"性能"两方面介绍TCP的机制. 安全和性能, 追求一个服务端和客户端双方都能接受的一个平衡点(一味的追求性能, 则不够安全;一味的追求安全则性能不够好)。

保证"安全"的机制:

1. 确认应答机制

2. 超时重传机制

3. 流量控制机制(针对的是"接收端缓冲区"的安全)

4. 拥塞控制机制(针对的是"发送端"的安全)

保证"性能"的机制:

1. 滑动窗口(针对的是"发送端"的性能)

2. 捎带应答机制

3. 延迟应答机制

1. 确认应答机制(ACK机制)

        如上图, 主机A每传输一个数据, 主机B收到了都会进行回复, 主机B确认了这个消息, 才能往下发下一条消息。 这是一个串型的过程, 是一个确认应答的机制。

        TCP将每个字节的数据都进行了编号, 即为序列号。

        每个序列号对应每个确认序列号,每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据, 下一次你从哪里开始发。

2. 超时重传机制

        因此主机B会收到很多重复数据, 那么TCP协议需要能够识别出哪些包是重复的包, 并且把重复的包丢弃掉.这时候我们可以利用在确认应答机制提到的序列号, 因为每个数据都有自己的编号, 就可以很容易做到去重的效果.

        那么, 如果超时的时间如何确定呢?

        最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回". 但是这个时间的长短, 随着网络环境的不同, 是有差异的.如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包. TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.如果重发一次后, 仍然得不到应答, 等待2*500ms后再进行重传; 如果仍然得不到应答, 等待4*500ms进行重传, 依次类推, 以指数形式递增.累计到一定的重传次数, TCP会认为网络或者对端主机出现异常, 强制关闭连接.

3. 连接管理机制

        TCP三次握手和四次挥手过程发生在TCP的"连接管理机制"中

1).三次握手和四次挥手过程中需要用到的三个标志位:

  • SYN: 请求建立连接; 双方都需要建立连接
  • ACK: 确认数据是否收到. (收到发送1, 没收到发送0)
  • FIN: 结束标志位.(携带这个标志位, 置为1表示关闭当前的连接)

        一般发送数据的称为客户端, 接收数据的称为服务端, 但是没有绝对的称呼.

2).建立连接的请求(3次握手的过程)

        当客户端向服务端发送请求数据时, 客户端的数据会携带SYN标志位, 请求与服务端建立连接; 而此时只能表示客户端向服务端发送了建立连接的请求, 但是并不知道服务端是否愿意与客户端建立连接.

        此时服务端会向客户端发送一个携带ACK标志位的回复, 表示服务端答应与客户端建立连接. 等于就是给客户端一个回复"我答应你啦".

        到这里, 只完成了客户端到服务端所建立的单向连接; 服务端到客户端还没有建立链接呢.

        因此, 服务端也会发送一个携带SYN标志位的数据给客户端, 请求与客户端建立连接;

        这时候有两个标志位(一个是ACK,一个是SYN)都要去客户端那里, 为了提高效率, 于是ACK就可以搭个顺风车跟着SYN一起回客户端那里(这里体现了TCP另一个机制: 捎带应答机制), 表示服务端发送数据时可以将两个标志位合并在一起将数据发送给客户端.

        当客户端收到了来自服务端的" 带着ACK标志位的回复 "以及" 带着SYN标志位的请求 "时, 对于确认建立连接的回复就收下啦, 不再做处理; 而对于建立连接的请求, 同样的, 则会发送一个携带ACK标志位的进行确认, 表示愿意和服务端建立连接.

        此时, 已建立连接(三次握手过程结束).

3). 关闭连接的请求(四次挥手过程)

        当客户端想要关闭和服务端的连接时, 会发送一个携带FIN关闭连接的请求给服务端, 当服务端接收到这个请求时, 答应关闭连接时, 会发送一个携带ACK标志位的数据给客户端进行确认, 表示收到了客户端想要关闭连接的请求;

        但是这个关闭连接的请求仍然是客户端单方面发送给服务端的, 表示客户端到服务端已经关闭, 此时还需要服务端向客户端发送关闭连接的请求, 才能彻底关闭.

        因此服务端向客户端发送携带一个FIN标志位关闭连接的请求, 同样客户端收到了这个关闭连接的请求后会向服务端发送携带一个ACK标志位的回复, 表示确认收到了服务端关闭连接的请求, 来进行双方的关闭.

        以上, 已关闭连接(四次挥手过程结束)

4).TCP建立连接握手为什么是3次?为什么可以合并, 捎带应答把数据一起发送过去? 而为什么关闭连接挥手的过程是4次?为什么不能合并呢?

        对于3次握手建立连接中, ACK和SYN可以合并, 是因为ACK和SYN在建立连接时是同一个数据包发送过去的, 都在一个报文里面, 可以合并, 将两个标志位都置为1即可

        对于4次挥手关闭连接中, 是不能将ACK和FIN同时置为1的, 因为一个是操作系统内进行关闭的, 自己程序控制不了. 而另一个是用户进程自己调用close方法自动关闭的. 两个部分不是一个主体完成, 两个部分无法合并.

5). 为什么需要四次挥手?

        MSL是TCP报文里面最大生存时间, 它是任何报文段被丢弃前在网络内的最长时间.

四次挥手, 就是客户端和服务端分别释放连接的过程.

        客户端在发送完最后一次确认之后, 还要等待2MSL的时间.

        主要有两个原因, 一个是为了让服务端能够按照正常步骤进入CLOSED状态, 二是为了防止已经失效的请求连接报文出现在下次连接中.

(1). 由于客户端最后一个ACK可能会丢失. 这样服务端就无法正常进入CLOSED状态. 于是服务端会重传请求释放的报文, 而此时客户端如果已经关闭了, 那就收不到服务端的重传请求, 就会导致服务端不能正常释放. 而如果客户端还在等待时间内, 就会收到服务端的重传, 然后进行应答, 这样服务端就可以进入CLOSED状态了.

(2). 在这2MSL等待时间里, 本次连接的所有报文都已经从网络中消失, 从而不会出现在下次连接中.

4. 滑动窗口

        滑动窗口保证的是 "发送端"的性能.

        前面介绍了确认应答机制, 对每一个发送的数据段, 都要给一个ACK确认应答, 收到ACK之后, 再发送下一个数据段, 这样做有一个比较大的缺陷, 就是性能较差, 尤其是数据往返的时间较长的时候。

         如上图, 这样一发一收的方式性能较低, 那么可以一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)

        窗口大小指的是无需等待确认应答而可以继续发送数据的最大值, 上图中的窗口大小就是4000个字节(4个段)

        发送前4个段的时候,不需要等待任何ACK, 直接发送.

        收到第一个ACK后, 滑动窗口向后移动, 继续发送第5个段的数据, 以此类推.

        操作系统内核为了维护这个滑动窗口, 需要开辟 "发送缓冲区" 来记录当前还有哪些数据没有应答, 只有确认应答过的数据, 才能从缓冲区删掉.

        窗口越大, 则网络的吞吐率就越高;

        窗口不会因为丢失的数据包扩大, 可能会因为网络好一些(或其他外部原因), 窗口扩大

        窗口是动态可以变化的, 可以根据很多条件来进行动态调节的, 并不是固定不变的

        窗口可以根据数据包大小进行调整(即窗口滑动).例如: 开始窗口是从1001-5000的数据, 数据发送过程中只哟1001-2000中间的数据发送过去后收到了响应, 那么此时窗口就会右滑到2001, 此时窗口的大小就调整为了2001-6000.

        那么如果出现了丢包, 如何进行重传呢? 这里分两种情况进行讨论.

情况一: 发送过去的数据包已经抵达, 传输回来的时候响应ACK被丢了.

        这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认.

情况二: 发送过去的数据包丢了

        当某一段报文丢失之后, 发送端会一直收到1001这样的ACK, 提醒发送端 "我想要1001".如果发送端主机连续三次收到了同样一个1001这样的应答, 就会将对应的数据1001-2000重新发送. 这时候接收端收到了1001之后, 再次向发送端返回的ACK就是7001了(因为2001-7000的数据, 接收端已经收到了, 已经被放到了接收端操作系统内核的接收缓冲区中).

5. 延迟应答机制

        发送端将数据发送给接收端后, 如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.

        假设接收端缓冲区为1M, 一次收到了500K的数据, 如果立刻应答, 返回的窗口就是500K, 但实际上可能处理端处理数据的速度很快, 10ms之内就把500K数据从缓冲区消费掉了.

        在这种情况下, 接收端处理还远远没有达到自己的极限, 即使窗口再放大一些, 也能处理的过来. 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M.窗口越大, 网络吞吐量就越大, 传输效率就越高, 延迟应答的目的就是 保证网络不拥塞的情况下尽量提高传输效率.

        那么所有的包都可以延迟应答吗? 肯定不是. 延迟应答有以下两个限制:

数量限制: 每隔n个包就应答一次.

时间限制: 超过最大延迟时间就应答一次.

        具体的数量和超时时间, 根据操作系统来定; 一般N取2, 超时时间取200ms

6. 捎带应答机制

        在延迟应答的基础上, 会发现很多情况下, 客户端服务器在应用层也是"一发一收"的. 意味着客户端给服务器说了"How are you?"服务器也会给客户端回一个"Fine, thank you." 那么这个时候ACK就可以搭顺风车, 和服务器回应的"Fine, thank you."一起回给客户端. 响应可以将数据一起捎带发送过去, 降低网络传输的风险, 这是为了提高传输效率.

        在TCP三次握手过程中, 第二次握手时, 服务端返回给客户端的ACK响应和想要跟客户端建立连接的请求SYN就体现了捎带应答机制.

7. 流量控制机制

流量控制保证了接收端缓冲区的"安全"

        接收端处理数据的速度是有限的, 如果发送端发的太快, 导致接收端的缓冲区已经满了, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等一系列的连锁反应.

        因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度, 这个机制就叫做 流量控制(Flow Control);

        接收端将自己可以接收的缓冲区大小放入TCP首部中的"窗口大小"字段, 通过ACK端通知发送端; 窗口大小字段越大, 说明网络的吞吐量越高; 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;发送端接收到这个窗口后, 就会减慢自己的发送速度. 如果接收端缓冲区满了, 就会将窗口置为0, 这时发送方不再发送数据, 但是需要定期发送一个窗口进行探测数据段, 使接收端把窗口大小告诉发送端.

        接收端如何把窗口大小告诉发送端呢? 在TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息.

8. 拥塞控制机制

        保证的是发送端的安全。网络状态不太好的时候, 拥塞控制就派上了用场。虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据, 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.

        因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵, 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.

        TCP引入慢启动机制, 先发少量的数据,探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据.

此处引入一个概念为拥塞窗口.

        发送开始的时候, 定义拥塞窗口大小为1; 每次收到一个ACK应答, 拥塞窗口大小+1; 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小作比较, 取较小的值作为实际发送的窗口;

        拥塞窗口的增长速度, 是指数级别增长的, "慢启动"只是指初始时慢, 但是增长速度非常快. 为了不增长那么快, 因此不能使拥塞窗口单纯的加倍增长.

        此处再引入一个叫做 "慢启动" 的阈值, 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长.

        当TCP开始启动的时候, 慢启动阈值等于窗口最大值. 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置为1.少量的丢包, 仅仅是会触发超时重传, 但大量丢包, TCP就会认为网络拥塞.

        当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降.拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.要根据拥塞控制和滑动窗口两方面的考虑, 决定发送数据包的大小怎么样是合理的。

9. TCP面向字节流

        UDP是面向数据报, 发送一次报文, 就结束了, 报文是一块一块的, 而TCP不是这样的,TCP是面向字节流.

        TCP既有发送缓冲区又有接收缓冲区, 而UDP没有发送缓冲区。

        创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区.

        调用write时, 数据会先写入发送缓冲区中; 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出; 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去.

        接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区. 然后应用程序可以调用read从接收缓冲区拿数据. 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个链接, 既可以读数据, 也可以写数据, 这个概念叫做全双工。

        由于缓冲区的存在, TCP程序的读和写不需要一一匹配,

         例如: 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read100个字节, 也可以一次read一个字节, 重复100次.

10. 粘包问题

        首先要明确, 粘包问题中的 "包", 指的是应用层的数据包.

        在TCP的协议头中, 没有如同UDP一样的"报文长度"这样的字段, 但是有一个序号这样的字段.站在传输层的角度, TCP是一个一个报文过来的, 按照序号排好序放在缓冲区里.

站在应用层的角度, 看到的只是一串连续的字节数据.

        那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分到哪个部分是一个完整的应用层数据包了.

        那么如何避免粘包问题呢? 一句话: 明确两个包之间的边界。

        对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可.

        对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置. 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和中文冲突即可)

思考: 对于UDP协议来说, 是否也存在"粘包问题"呢?

        对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在, 同时UDP是一个一个把数据交付给应用层, 就有很明确的数据边界. 站在应用的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收, 不会出现"半个"的情况.

11. TCP异常情况

        进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN, 和正常的关闭没有什么区别.

机器重启: 和进程终止的情况相同.

        机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset, 即使没有写入操作,TCP自己也内置了一个保护定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.

        另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如:QQ.在QQ断线之后, 也会定期尝试重新连接.

12. 基于TCP应用层协议

        基于TCP应用层的协议有: HTTP, HTTPS, SSH, Telnet, FTP, SMIP.也包括个人写TCP程序时自定义的应用层协议。

以上是关于TCP协议各项机制详解的主要内容,如果未能解决你的问题,请参考以下文章

TCP协议数据传输的基本机制:滑动窗口运行过程详解

Tcp协议保证可靠传输机制

Tcp协议保证可靠传输机制

TCP协议如何保证数据可靠性

TCP协议三次握手与四次挥手详解(上)

TCP协议三次握手与四次挥手详解(上)