TCP 可靠性的关键机制 —— 确认应答机制 (ACK)

确认应答机制是 实现TCP可靠性的关键机制,简单来说,确认应答机制就是,客户端和服务端任意一方,在发送消息之后,都必须要收到对方的回复来表明自己发送的消息已经被对方收到。

TCP可靠性不仅仅在于发送的消息被对方收到,还必须是被对方按序接收。下面就来了解确认应答机制是如何满足这两点要求的。


目录

1、TCP可靠性的两个要求

2、确认应答机制的实现方式

3、如何理解序号?

4、为什么序号和确认序号是两个独立的字段?


1、TCP可靠性的两个要求

可靠性的第一个要求,发送方确保自己的数据被对方收到。发送方发送一条消息以后,接收方需要回复确认表明自己已经收到了消息,这样的话发送方就知道自己发送的消息已经被对方收到了。无论是客户端还是服务端,在发送数据的时候,只要收到了对方的回复,就保证了当前方向的数据被对方接收的可靠性。

可靠性的第二个要求,发送方发送报文的顺序必须要和接收方一致。Client端可以给Server端发送一批报文,假设这批报文对应的序号是1、2、3,然而如果接收端的序号是3、1、2,接收端根据收到的消息做出回复,可能会产生很严重的歧义。就像下面这样,Client端发送的消息最后的结果是“不想吃”,但是因为网络问题、路由路线问题,服务端接收到的顺序变成了3、1、2,导致最后的结果是“等会在附近吃”。

2、确认应答机制的实现方式

首先是保证接收方接收到的消息不是乱序的。这就需要用到TCP首部里的一个字段“32位序号”,发送方在首部里添加序号,接收方根据首部里的序号按序接收,读完 1号消息再读 2号消息,这样就保证了接收的顺序性,进而保证了回复时不会乱序。

其次是接收方回复对应序号的报文。接收方在收到报文以后,要回复对方收到报文,这个时候就要用到TCP首部里的另一个字段“32位确认序号”。假设收到了序号为10 的报文,此时确认序号应该是11,表明前10号报文我已经收到了,你可以发送第11号报文了。发送方收到确认报文以后,可以通过确认序号来辨别哪一个报文被对方接收了。

3、如何理解序号?

虽然说发送请求的时候,报文会携带序号,这里的序号到底是指什么?

我们可以把TCP缓冲区理解成一个 大数组,数组的每个位置都是以字节为单位的。上层调用send接口函数时,其实就是把数据拷贝到发送缓冲区中,假设上层要发送“Hello”,拷贝到缓冲区以后,字符串的每个字节都被自动标上了数组的下标,最后一个字符的下标是4,那么报文首部的序号就是 4,即最后一个字节的下标。

Server会发送应答报文,报文里的确认序号就是 5,其实就是在告诉Client,下次你要从数组下标为5的字节开始发。

4、为什么序号和确认序号是两个独立的字段?

发送方发送的数据要携带序号,接收方回复的时候要携带确认序号,那么为什么要把序号和确认序号分开而不是使用一个统一的字段呢??

TCP协议是一种全双工的通信方式。所谓全双工,就是客户端和服务端任意一方,在发消息的同时,也可以接收消息。服务端某个时刻要对客户端的消息进行确认,此时就要用到确认序号;与此同时,如果服务端还想给客户端发送数据,此时就要用到序号。

因此,正因为这种全双工的方式,序号和确认序号需要分开。

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TCP选择性应答(SACK)是一种TCP拥塞控制算法,它可以在网络拥塞时提高TCP的性能。下面给出一个简单的C语言实现TCP SACK机制的算法: ```c #include <stdio.h> #include <stdlib.h> #define MAX_SEQ_NUM 1000 // 最大序列号 #define MAX_WINDOW_SIZE 10 // 最大窗口大小 struct packet { int seq_num; // 数据包的序列号 int ack_num; // 确认号 int options[MAX_WINDOW_SIZE]; // 选项字段,用于存储SACK信息 }; int main() { int sent_base = 0; // 已发送但未确认的第一个数据包的序列号 int next_seq_num = 0; // 下一个待发送的数据包的序列号 int window_size = 3; // 当前窗口大小 int received_ack = -1; // 最近接收到的确认号 int acked[MAX_SEQ_NUM] = {0}; // 已确认的数据包 // 模拟发送和接收数据包 while (sent_base < MAX_SEQ_NUM) { // 发送数据包 if (next_seq_num < sent_base + window_size) { // 构造数据包 struct packet pkt; pkt.seq_num = next_seq_num; pkt.ack_num = received_ack; // 发送数据包 send(pkt); // 更新下一个待发送的数据包的序列号 next_seq_num++; } // 接收确认包 struct packet ack_pkt; if (receive(&ack_pkt)) { // 如果确认包的确认号大于等于sent_base,则更新sent_base if (ack_pkt.ack_num >= sent_base) { int i; for (i = sent_base; i <= ack_pkt.ack_num; i++) { acked[i] = 1; // 标记已确认的数据包 } sent_base = ack_pkt.ack_num + 1; // 更新sent_base received_ack = ack_pkt.ack_num; // 更新最近接收到的确认号 } // 处理SACK信息 int j; for (j = 0; j < MAX_WINDOW_SIZE; j++) { if (ack_pkt.options[j] >= sent_base && ack_pkt.options[j] < next_seq_num) { acked[ack_pkt.options[j]] = 1; // 标记已确认的数据包 } } } // 更新窗口大小 int i; for (i = sent_base; i < next_seq_num; i++) { if (!acked[i]) { // 如果该数据包未被确认 break; } } window_size = i - sent_base; // 更新当前窗口大小 } return 0; } ``` 上述实现中,使用sent_base来表示已发送但未确认的第一个数据包的序列号,使用next_seq_num来表示下一个待发送的数据包的序列号,使用window_size来表示当前窗口大小,使用received_ack来表示最近接收到的确认号,使用acked数组来记录已确认的数据包。 在发送数据包时,如果下一个待发送的数据包的序列号小于sent_base+window_size,则发送该数据包,并更新next_seq_num。在接收确认包时,如果确认号大于等于sent_base,则更新sent_base和received_ack,并标记acked数组中对应的数据包。如果确认包中包含SACK信息,则同样标记acked数组中对应的数据包。 最后更新窗口大小的逻辑是,从sent_base开始遍历acked数组,找到第一个未确认的数据包的序列号,并将当前窗口大小设置为该数据包序列号与sent_base之差。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值