TCP(传输控制协议)的核心特性是提供可靠的数据传输服务,即确保数据能够无错、完整、按序地从发送方传输到接收方。这种可靠性是通过一系列精心设计的机制实现的,包括序列号与确认号机制、超时重传机制、滑动窗口机制和差错控制机制等。本章将深入探讨这些机制的原理和实现。
序列号和确认号是TCP可靠传输的基础,它们使得TCP能够跟踪已发送和已接收的数据,检测丢失或重复的数据,并确保数据按序交付。
序列号(Sequence Number):用于标识从TCP发送端向TCP接收端发送的数据字节流的位置。序列号是32位的无符号整数,当它达到2^32-1时会绕回到0。
确认号(Acknowledgment Number):用于告知发送方接收方已成功接收的数据。确认号表示接收方期望从发送方收到的下一个字节的序列号,即已成功接收的数据的最高序列号加1。
在TCP连接建立时,通过三次握手过程,双方会交换随机生成的初始序列号(ISN),这是为了安全考虑,防止序列号被猜测和连接被劫持。
TCP将数据视为无结构的字节流,每个字节都有一个序列号。当发送数据时,TCP会为每个数据段分配序列号,接收方则通过确认号告知发送方已成功接收的数据。
以下是序列号与确认号工作的基本流程:
发送方发送数据:
接收方接收数据:
发送方处理确认:
这个过程确保了数据的可靠传输:如果数据段丢失,接收方不会确认该段,发送方会在超时后重传;如果数据段乱序到达,接收方可以根据序列号重新排序;如果数据段重复到达,接收方可以根据序列号识别并丢弃重复的数据。
在Linux内核中,序列号和确认号的处理涉及多个数据结构和函数。以下是一些关键的实现细节:
TCP控制块(TCP Control Block,TCB)是存储TCP连接状态的数据结构,在Linux内核中对应struct tcp_sock
。它包含了与序列号和确认号相关的多个字段:
// file: include/linux/tcp.h
struct tcp_sock {
// ...
/* 发送相关 */
u32 snd_una; /* 已发送但未确认的第一个字节的序列号 */
u32 snd_nxt; /* 下一个要发送的字节的序列号 */
u32 snd_wl1; /* 用于窗口更新的序列号 */
u32 snd_wl2; /* 用于窗口更新的确认号 */
u32 write_seq; /* 初始发送序列号 */
/* 接收相关 */
u32 rcv_nxt; /* 期望接收的下一个字节的序列号 */
u32 rcv_wup; /* 接收窗口更新点 */
u32 copied_seq; /* 已复制到用户空间的最后一个字节的序列号 */
// ...
};
在TCP连接建立时,需要为连接分配初始序列号(ISN)。Linux内核使用secure_tcp_seq
函数生成随机的初始序列号:
// 简化版的secure_tcp_seq函数
__u32 secure_tcp_seq(__be32 saddr, __be32 daddr, __be16 sport, __be16 dport)
{
struct tcp_secret secret;
u32 hash[MD5_DIGEST_WORDS];
u32 seq;
// 使用密钥和连接四元组计算哈希值
net_secret_init(&secret);
seq = secret.secrets[0];
seq += ktime_get_real_ns() >> 6;
seq += saddr;
seq += daddr;
seq += (sport << 16) + dport;
// 添加随机性
seq ^= secure_tcp_sequence_number(hash);
return seq;
}
这个函数使用源IP地址、目的IP地址、源端口、目的端口以及一个密钥和时间戳来生成随机的初始序列号,增加了序列号被猜测的难度,提高了安全性。
当发送数据时,TCP需要为数据段分配序列号。这通常在tcp_transmit_skb
函数中完成:
// 简化版的tcp_transmit_skb函数
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
// ...
// 构建TCP头部
th = tcp_hdr(skb);
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
// 设置序列号
th->seq = htonl(tcb->seq);
// 设置确认号(如果需要)
if (tcb->tcp_flags & TCPHDR_ACK)
th->ack_seq = htonl(tp->rcv_nxt);
// ...
// 更新发送序列号
if (!(tcb->tcp_flags & TCPHDR_SYN))
tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;
// ...
}
当接收数据时,TCP需要处理序列号和生成确认号。这通常在tcp_rcv_established
函数中完成:
// 简化版的tcp_rcv_established函数
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 检查序列号是否在接收窗口内
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
// 序列号不在接收窗口内,丢弃数据段
// ...
goto discard;
}
// 处理数据
tcp_data_queue(sk, skb);
// 更新接收序列号
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
// 发送确认
tcp_send_ack(sk);
// ...
}
在tcp_send_ack
函数中,会构建并发送ACK包,确认号设置为tp->rcv_nxt
:
// 简化版的tcp_send_ack函数
void tcp_send_ack(struct sock *sk)
{
// ...
// 构建ACK包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
if (!buff)
return;
// 设置TCP头部
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tp->snd_una, TCPHDR_ACK);
// 设置确认号
th = tcp_hdr(buff);
th->ack_seq = htonl(tp->rcv_nxt);
// 发送ACK包
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
// ...
}
序列号和确认号机制在TCP的实际应用中发挥着关键作用,它们支持了多种重要功能:
序列号使得接收方能够按照发送顺序重组数据,即使数据段乱序到达。接收方会根据序列号将数据段放入正确的位置,确保应用程序收到的数据与发送时的顺序相同。
通过确认号,发送方可以知道哪些数据已经被接收方成功接收。如果在一定时间内没有收到某个数据段的确认,发送方会认为该数据段丢失,并重新发送。
如果接收方收到了重复的数据段(可能是由于网络延迟或重传导致的),它可以根据序列号识别出这是重复的数据,并丢弃它,避免将重复的数据交付给应用程序。
序列号和确认号是TCP流量控制和拥塞控制的基础。通过跟踪已发送和已确认的数据量,TCP可以调整发送速率,避免接收方缓冲区溢出和网络拥塞。
在标准的TCP中,确认号只能确认连续的数据。选择性确认(SACK)扩展了这一机制,允许接收方确认非连续的数据块,提高了重传效率。SACK通过TCP选项实现,使用序列号范围来指示已接收的数据块。
序列号是32位的无符号整数,当发送的数据量超过4GB时,序列号会回绕到0。这可能导致新旧数据无法区分,特别是在高速网络中。
TCP通过时间戳选项解决了这个问题。时间戳选项为每个数据段添加一个时间戳,即使序列号回绕,也可以通过时间戳区分新旧数据。这个机制被称为PAWS(Protection Against Wrapped Sequence numbers)。
如果初始序列号是可预测的,攻击者可能会猜测序列号并伪造TCP段,导致连接劫持或数据注入。为了提高安全性,现代TCP实现使用加密算法生成随机的初始序列号,如前面介绍的secure_tcp_seq
函数。
为了减少网络开销,TCP通常不会立即确认每个接收到的数据段,而是延迟一段时间(通常是200ms),希望在这段时间内有数据要发送,可以将确认捎带在数据段中。这种延迟确认机制可能会增加重传超时的可能性。
优化方法包括:
如果接收方收到了乱序的数据段,它会发送重复确认(duplicate ACK),确认号仍然是期望接收的下一个字节的序列号。当发送方收到多个重复确认(通常是3个)时,会触发快速重传机制,立即重传可能丢失的数据段,而不等待重传超时。
这个机制在Linux内核中的实现如下:
// 简化版的tcp_fastretrans_alert函数
void tcp_fastretrans_alert(struct sock *sk, int pkts_acked, int flag)
{
// ...
// 检查是否收到了3个重复确认
if (tp->dup_ack >= tp->reordering) {
// 触发快速重传
tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
// 进入快速恢复状态
tp->snd_cwnd = tp->snd_ssthresh + tp->reordering;
// ...
}
// ...
}
序列号和确认号机制是TCP可靠传输的基础,它们通过精心设计的交互过程,确保了数据的可靠、有序传输。理解序列号和确认号的原理和实现,对于深入理解TCP协议和优化网络应用都有重要意义。
超时重传机制是TCP可靠传输的关键组成部分,它确保了在数据包丢失的情况下,发送方能够检测到丢失并重新发送数据,从而保证数据的可靠传输。
**超时重传(Timeout Retransmission)**是指当发送方在一定时间内没有收到已发送数据的确认时,假定数据已丢失,并重新发送该数据的机制。
超时重传机制基于以下假设:
超时重传的关键是确定合适的超时时间,即重传超时时间(Retransmission Timeout,RTO)。如果RTO设置得太短,可能会导致不必要的重传,增加网络负担;如果RTO设置得太长,则会延长数据恢复的时间,降低传输效率。
TCP超时重传的基本工作流程如下:
发送数据:
等待确认:
超时重传:
重传限制:
TCP的超时重传机制不仅仅是简单地重新发送数据,还包括了RTO的计算和调整,以适应网络状况的变化。
RTO的计算是TCP超时重传机制的核心部分。TCP使用往返时间(Round-Trip Time,RTT)的测量值来计算RTO。RFC 6298定义了标准的RTO计算算法,称为Jacobson/Karels算法:
初始化:
首次RTT测量:
后续RTT测量:
RTO限制:
指数退避:
这个算法使得RTO能够适应网络状况的变化:当网络稳定时,RTO接近RTT;当网络不稳定时,RTO会增大,减少不必要的重传。
在Linux内核中,超时重传机制的实现涉及多个函数和数据结构。以下是一些关键的实现细节:
Linux内核使用类似于RFC 6298的算法计算RTO,但有一些优化和调整:
// 简化版的tcp_rtt_estimator函数
void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
struct tcp_sock *tp = tcp_sk(sk);
long m = mrtt_us;
u32 srtt = tp->srtt_us;
// 更新SRTT和RTTVAR
if (srtt != 0) {
// 已有SRTT,使用EWMA算法更新
m -= (srtt >> 3); // m = rtt - srtt/8
srtt += m; // srtt += m/8
if (m < 0) {
m = -m;
m -= (tp->mdev_us >> 2);
if (m > 0)
m >>= 3;
} else {
m -= (tp->mdev_us >> 2);
if (m > 0)
m >>= 2;
}
tp->mdev_us += m; // mdev += m/4
// 更新最大MDEV
if (tp->mdev_us > tp->mdev_max_us) {
tp->mdev_max_us = tp->mdev_us;
if (tp->mdev_max_us > tp->rttvar_us)
tp->rttvar_us = tp->mdev_max_us;
}
// 每隔一段时间,更新RTTVAR
if (after(tp->snd_una, tp->rtt_seq)) {
tp->rtt_seq = tp->snd_nxt;
if (tp->mdev_max_us < tp->rttvar_us)
tp->rttvar_us -= (tp->rttvar_us - tp->mdev_max_us) >> 2;
tp->mdev_max_us = tcp_rto_min_us(sk);
}
} else {
// 首次测量RTT
srtt = m << 3;
tp->mdev_us = m;
tp->mdev_max_us = m;
tp->rttvar_us = m << 1;
}
tp->srtt_us = srtt;
// 计算RTO
tp->rto = __tcp_set_rto(tp);
// 限制RTO的范围
tp->rto = min(tp->rto, TCP_RTO_MAX);
tp->rto = max(tp->rto, TCP_RTO_MIN);
}
TCP使用定时器来触发超时重传。在Linux内核中,这通过tcp_write_timer
函数实现:
// 简化版的tcp_write_timer函数
void tcp_write_timer(struct timer_list *t)
{
struct inet_connection_sock *icsk = from_timer(icsk, t, icsk_retransmit_timer);
struct sock *sk = &icsk->icsk_inet.sk;
// 检查是否需要重传
if (!sock_owned_by_user(sk)) {
tcp_retransmit_timer(sk);
} else {
// 如果套接字正在被用户使用,延迟重传
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
icsk->icsk_rto, TCP_RTO_MAX);
}
sock_put(sk);
}
当重传定时器超时时,会调用tcp_retransmit_timer
函数处理:
// 简化版的tcp_retransmit_timer函数
void tcp_retransmit_timer(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
// 检查是否有未确认的数据
if (!tp->packets_out)
goto out;
// 检查是否超过最大重传次数
if (tcp_write_timeout(sk))
goto out;
// 执行超时重传
tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
// 更新RTO(指数退避)
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
// 重新启动重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
icsk->icsk_rto, TCP_RTO_MAX);
// ...
out:
// ...
}
当需要重传数据段时,会调用tcp_retransmit_skb
函数:
// 简化版的tcp_retransmit_skb函数
int tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb)
{
// ...
// 准备重传
TCP_SKB_CB(skb)->seq = tp->snd_una;
// 更新重传计数
TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_SYN;
tp->retrans_stamp = tcp_time_stamp(tp);
tp->retrans_out += tcp_skb_pcount(skb);
// 发送重传的数据段
err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
// ...
return err;
}
超时重传机制在TCP的实际应用中发挥着关键作用,它确保了数据的可靠传输,特别是在网络状况不佳的情况下。以下是一些实际应用场景:
在网络拥塞或链路质量差的情况下,数据包可能会丢失。超时重传机制能够检测到这种丢失,并重新发送数据,确保数据的完整性。
即使数据包成功到达接收方,确认包也可能在返回途中丢失。超时重传机制同样能够处理这种情况,通过重新发送数据来触发新的确认。
通过动态调整RTO,TCP能够适应网络状况的变化。当网络延迟增加时,RTO会相应增加,减少不必要的重传;当网络状况改善时,RTO会逐渐减小,提高传输效率。
超时重传机制与快速重传机制协同工作,共同提高TCP的可靠性和效率。快速重传通过重复确认检测丢失,能够更快地响应;而超时重传作为最后的保障,确保即使在没有足够重复确认的情况下,丢失的数据也能被恢复。
如果RTO设置得不合理,或者网络延迟突然增加,可能会导致虚假超时,即数据实际上没有丢失,但由于确认没有在RTO时间内到达,触发了不必要的重传。
优化方法包括:
当发生重传后,如果收到确认,无法确定这个确认是针对原始传输还是重传。这被称为重传歧义问题,它会影响RTT的测量和RTO的计算。
解决方法包括:
在网络严重拥塞的情况下,大量的数据包可能会丢失,导致大规模的重传。如果所有连接同时进行重传,可能会加剧网络拥塞,形成重传风暴。
解决方法包括:
不同的网络环境(如有线网络、无线网络、卫星链路等)有不同的特性,标准的超时重传机制可能不是最优的。针对特定网络环境的优化包括:
超时重传机制是TCP可靠传输的重要组成部分,它通过检测和恢复丢失的数据,确保了数据的完整传输。理解超时重传的原理和实现,对于优化网络应用和解决网络问题都有重要意义。
滑动窗口机制是TCP流量控制和可靠传输的核心机制,它允许发送方在收到确认前发送多个数据段,提高了网络利用率,同时防止发送方发送过多数据导致接收方缓冲区溢出。
**滑动窗口(Sliding Window)**是一种流量控制机制,它定义了在任意时刻,发送方可以发送的数据量。窗口的大小是动态调整的,取决于接收方的处理能力和网络状况。
TCP使用两种窗口:
滑动窗口机制的核心思想是:发送方可以在收到确认前发送多个数据段,但发送的数据量不能超过当前的窗口大小。随着数据的发送和确认,窗口会沿着数据流"滑动",这就是"滑动窗口"名称的由来。
发送窗口可以分为四个部分:
发送窗口的大小取决于接收方通告的接收窗口大小和发送方的拥塞窗口大小,取两者的较小值。
接收窗口也可以分为三个部分:
接收窗口的大小取决于接收方的缓冲区大小和当前已使用的缓冲区空间。
滑动窗口的工作过程如下:
初始状态:
发送数据:
接收确认:
接收窗口更新:
这个过程确保了发送方不会发送超过接收方能够处理的数据量,实现了流量控制。
在Linux内核中,滑动窗口机制的实现涉及多个数据结构和函数。以下是一些关键的实现细节:
TCP控制块(struct tcp_sock)包含了与滑动窗口相关的多个字段:
// file: include/linux/tcp.h
struct tcp_sock {
// ...
/* 发送窗口相关 */
u32 snd_una; /* 已发送但未确认的第一个字节的序列号 */
u32 snd_nxt; /* 下一个要发送的字节的序列号 */
u32 snd_wnd; /* 发送窗口大小 */
u32 snd_wl1; /* 用于窗口更新的序列号 */
u32 snd_wl2; /* 用于窗口更新的确认号 */
u32 write_seq; /* 初始发送序列号 */
/* 接收窗口相关 */
u32 rcv_nxt; /* 期望接收的下一个字节的序列号 */
u32 rcv_wnd; /* 接收窗口大小 */
u32 rcv_wup; /* 接收窗口更新点 */
// ...
};
发送窗口的大小由接收方通告的窗口大小和拥塞窗口大小共同决定:
// 简化版的tcp_current_mss函数
u32 tcp_wnd_end(const struct tcp_sock *tp)
{
return tp->snd_una + min(tp->snd_wnd, tp->snd_cwnd);
}
当发送数据时,TCP会检查数据是否在发送窗口内:
// 简化版的tcp_write_xmit函数
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
// ...
while ((skb = tcp_send_head(sk))) {
// 检查是否在发送窗口内
if (tcp_snd_wnd_test(tp, skb, mss_now))
break;
// 发送数据段
tcp_transmit_skb(sk, skb, 1, gfp);
// 更新发送序列号
tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;
// ...
}
// ...
}
接收窗口的大小取决于接收缓冲区的大小和当前已使用的空间:
// 简化版的tcp_select_window函数
u16 tcp_select_window(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 cur_win = tcp_receive_window(tp);
u32 new_win = __tcp_select_window(sk);
// 确保窗口不会收缩
if (new_win < cur_win) {
new_win = cur_win;
}
// 限制窗口大小
if (new_win > 65535)
new_win = 65535;
return new_win;
}
当接收数据时,TCP会检查数据是否在接收窗口内:
// 简化版的tcp_rcv_established函数
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
// ...
// 检查序列号是否在接收窗口内
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
// 序列号不在接收窗口内,丢弃数据段
// ...
goto discard;
}
// 处理数据
tcp_data_queue(sk, skb);
// 更新接收序列号
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
// 发送确认和窗口更新
tcp_send_ack(sk);
// ...
}
当接收方的窗口大小发生变化时,需要通知发送方。这通常通过发送ACK包实现:
// 简化版的tcp_send_ack函数
void tcp_send_ack(struct sock *sk)
{
// ...
// 构建ACK包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
if (!buff)
return;
// 设置TCP头部
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tp->snd_una, TCPHDR_ACK);
// 设置确认号
th = tcp_hdr(buff);
th->ack_seq = htonl(tp->rcv_nxt);
// 设置窗口大小
th->window = htons(tcp_select_window(sk));
// 发送ACK包
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
// ...
}
当发送方收到窗口更新时,会更新自己的发送窗口:
// 简化版的tcp_ack函数
static bool tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
// ...
// 检查是否需要更新窗口
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
// 更新发送窗口
tcp_update_wl(tp, ack_seq, ack);
tp->snd_wnd = nwin;
// 检查是否可以发送更多数据
tcp_fast_path_check(sk);
}
// ...
}
滑动窗口机制在TCP的实际应用中发挥着关键作用,它支持了多种重要功能:
滑动窗口的主要作用是实现流量控制,防止发送方发送过多数据导致接收方缓冲区溢出。通过动态调整窗口大小,接收方可以控制数据的接收速率,适应自己的处理能力。
传统的停等协议(发送一个数据包,等待确认后再发送下一个)在高延迟网络中效率很低。滑动窗口允许发送方在收到确认前发送多个数据段,提高了网络利用率,特别是在高延迟网络中。
滑动窗口机制允许接收方使用累积确认,即一个确认可以确认多个数据段。这减少了确认的数量,降低了网络开销。
滑动窗口机制与拥塞控制机制协同工作,共同决定发送方的发送速率。拥塞窗口限制了发送方向网络中注入的数据量,防止网络拥塞;而接收窗口限制了发送方向接收方发送的数据量,防止接收方缓冲区溢出。
TCP头部中的窗口大小字段是16位的,最大值为65535字节。这在高带宽延迟积网络中可能不够用。为了解决这个问题,TCP引入了窗口缩放选项,允许将窗口大小乘以一个缩放因子(最大为14),从而支持最大约1GB的窗口。
在Linux内核中,窗口缩放的实现如下:
// 简化版的tcp_select_window函数
u16 tcp_select_window(struct sock *sk)
{
// ...
// 应用窗口缩放
if (tp->rx_opt.rcv_wscale) {
new_win = new_win >> tp->rx_opt.rcv_wscale;
}
// ...
}
如果接收方的缓冲区已满,它会通告一个零窗口,告诉发送方暂停发送数据。当接收方处理了一些数据后,它需要通知发送方恢复发送。但如果这个窗口更新包丢失了,可能会导致死锁:发送方一直等待窗口更新,而接收方认为发送方已经知道窗口已经打开。
为了解决这个问题,TCP引入了零窗口探测机制:当发送方收到零窗口通告时,会启动一个持续定时器,定期发送一个字节的数据(称为窗口探测包),强制接收方重新通告窗口大小。
在Linux内核中,零窗口探测的实现如下:
// 简化版的tcp_send_probe0函数
void tcp_send_probe0(struct sock *sk)
{
// ...
// 构建探测包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
if (!buff)
return;
// 设置TCP头部
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tp->snd_una - 1, TCPHDR_ACK);
// 发送探测包
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
// 重新启动持续定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX),
TCP_RTO_MAX);
// ...
}
糊涂窗口综合症(Silly Window Syndrome)是指接收方只处理了少量数据就急于通告窗口更新,或发送方收到小窗口更新就急于发送小数据段,导致网络中充斥着小数据段,降低了传输效率。
解决方法包括:
在Linux内核中,这些优化的实现如下:
// 简化版的tcp_nagle_test函数
static bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,
unsigned int mss_now, int nonagle)
{
// 检查是否启用Nagle算法
if (nonagle & TCP_NAGLE_OFF)
return true;
// 如果有未确认的数据且数据大小小于MSS,延迟发送
if ((nonagle & TCP_NAGLE_CORK) && tp->packets_out)
return false;
// 标准Nagle算法
return (skb->len >= mss_now ||
!tcp_packets_in_flight(tp) ||
(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN));
}
为了适应不同的网络环境和应用需求,现代TCP实现通常会自动调整接收窗口的大小。Linux内核实现了接收窗口自动调整机制,根据应用程序的读取速率和网络状况动态调整接收窗口的大小。
// 简化版的tcp_rcv_space_adjust函数
void tcp_rcv_space_adjust(struct sock *sk)
{
// ...
// 计算新的接收窗口大小
if (tp->rcv_ssthresh < tp->window_clamp &&
(tp->rcv_ssthresh <= tp->rcv_wnd ||
(tp->rcv_wnd < tp->window_clamp &&
tp->rcv_wnd < tp->rcv_ssthresh + 2 * tp->advmss))) {
// 增加接收窗口
tp->rcv_ssthresh = min(tp->rcv_ssthresh + tp->advmss, tp->window_clamp);
tp->rcv_wnd = min(tp->rcv_wnd + tp->advmss, tp->window_clamp);
}
// ...
}
滑动窗口机制是TCP流量控制和可靠传输的核心机制,它通过动态调整窗口大小,实现了发送方和接收方之间的流量平衡。理解滑动窗口的原理和实现,对于优化网络应用和解决网络问题都有重要意义。
差错控制是TCP可靠传输的重要组成部分,它确保了数据在传输过程中的完整性和正确性。TCP通过多种机制检测和处理传输过程中可能出现的错误,包括校验和、确认和重传、序列号和确认号以及超时机制等。
**差错控制(Error Control)**是指检测和纠正传输过程中可能出现的错误的机制。在网络通信中,数据可能会因为多种原因而损坏或丢失,如信号衰减、电磁干扰、设备故障等。差错控制机制的目标是确保接收方能够正确地接收发送方发送的数据,或者至少能够检测到错误并请求重传。
TCP的差错控制主要解决以下几类问题:
TCP使用多种机制来实现差错控制,主要包括:
TCP使用校验和来检测数据在传输过程中是否被损坏。校验和是对TCP头部、数据以及一个伪头部(包含源IP地址、目的IP地址、协议号和TCP长度)进行计算得出的。
校验和的计算方法是:将所有16位字进行反码求和,然后取反码。接收方使用相同的算法计算校验和,如果结果为0,则认为数据完整;否则,认为数据已损坏,将丢弃该段。
校验和能够检测出大多数的位错误,但它不是一个强校验机制,某些特定模式的错误可能无法被检测出。为了提高可靠性,TCP通常与底层的错误检测机制(如以太网的CRC校验)结合使用。
TCP使用确认和重传机制来处理丢包问题。当发送方发送数据后,会等待接收方的确认;如果在一定时间内没有收到确认,发送方会假定数据已丢失,并重新发送。
TCP使用累积确认机制,即确认号表示接收方已成功接收的所有数据的最高序列号加1。这意味着一个确认可以确认多个数据段,减少了确认的数量,但也可能导致不必要的重传。
为了提高效率,TCP还支持选择性确认(SACK)机制,允许接收方确认非连续的数据块,减少不必要的重传。
TCP使用序列号和确认号来检测和处理重复和乱序的数据包。每个数据段都有一个唯一的序列号,接收方可以根据序列号识别重复的数据段并丢弃它们,也可以根据序列号重新排序乱序到达的数据段。
序列号和确认号机制确保了数据的有序交付,即使底层网络可能导致数据包乱序到达。
TCP使用超时机制来处理确认丢失的情况。当发送方在一定时间内没有收到确认时,会假定确认已丢失,并重新发送数据。
超时时间(RTO)是根据往返时间(RTT)的测量值动态计算的,以适应网络状况的变化。如果网络延迟增加,RTO会相应增加,减少不必要的重传;如果网络状况改善,RTO会逐渐减小,提高传输效率。
在Linux内核中,TCP的差错控制机制的实现涉及多个函数和数据结构。以下是一些关键的实现细节:
TCP校验和的计算在tcp_v4_send_check
函数中实现:
// 简化版的tcp_v4_send_check函数
void tcp_v4_send_check(struct sock *sk, struct sk_buff *skb)
{
struct inet_sock *inet = inet_sk(sk);
struct tcphdr *th = tcp_hdr(skb);
// 计算校验和
th->check = 0;
th->check = tcp_v4_check(skb->len, inet->inet_saddr, inet->inet_daddr,
csum_partial(th, skb->len, 0));
}
校验和的验证在tcp_v4_do_rcv
函数中实现:
// 简化版的tcp_v4_do_rcv函数
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
// ...
// 验证校验和
if (!pskb_may_pull(skb, sizeof(struct tcphdr)) ||
(skb_checksum_complete(skb))) {
goto discard_it;
}
// ...
}
TCP的确认机制在tcp_send_ack
函数中实现:
// 简化版的tcp_send_ack函数
void tcp_send_ack(struct sock *sk)
{
// ...
// 构建ACK包
buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
if (!buff)
return;
// 设置TCP头部
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tp->snd_una, TCPHDR_ACK);
// 设置确认号
th = tcp_hdr(buff);
th->ack_seq = htonl(tp->rcv_nxt);
// 发送ACK包
tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
// ...
}
重传机制在tcp_retransmit_timer
函数中实现:
// 简化版的tcp_retransmit_timer函数
void tcp_retransmit_timer(struct sock *sk)
{
// ...
// 检查是否有未确认的数据
if (!tp->packets_out)
goto out;
// 检查是否超过最大重传次数
if (tcp_write_timeout(sk))
goto out;
// 执行超时重传
tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
// 更新RTO(指数退避)
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
// 重新启动重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
icsk->icsk_rto, TCP_RTO_MAX);
// ...
out:
// ...
}
TCP的选择性确认机制在tcp_sacktag_write_queue
函数中实现:
// 简化版的tcp_sacktag_write_queue函数
int tcp_sacktag_write_queue(struct sock *sk, const struct sk_buff *ack_skb,
u32 prior_snd_una)
{
// ...
// 解析SACK选项
if (sack = TCP_SKB_CB(ack_skb)->sacked) {
// 处理SACK块
for (i = 0; i < num_sacks; i++) {
start_seq = sack[i].start_seq;
end_seq = sack[i].end_seq;
// 标记已被选择性确认的数据段
tcp_sacktag_one(sk, start_seq, end_seq, ...);
}
}
// ...
}
TCP的差错控制机制在实际应用中发挥着关键作用,确保了数据的可靠传输,特别是在网络状况不佳的情况下。以下是一些实际应用场景:
在网络拥塞或链路质量差的情况下,数据包可能会丢失。TCP的确认和重传机制能够检测到这种丢失,并重新发送数据,确保数据的完整性。
在复杂的网络环境中,数据包可能会走不同的路径,导致乱序到达。TCP的序列号机制能够重新排序这些数据包,确保应用程序收到的数据与发送时的顺序相同。
数据在传输过程中可能会因为各种原因而损坏。TCP的校验和机制能够检测到这种损坏,并丢弃损坏的数据包,触发重传。
通过动态调整RTO,TCP能够适应网络状况的变化。当网络延迟增加时,RTO会相应增加,减少不必要的重传;当网络状况改善时,RTO会逐渐减小,提高传输效率。
TCP校验和是一个相对简单的错误检测机制,它可能无法检测出某些特定模式的错误。此外,校验和的计算也会增加CPU负担。
优化方法包括:
当发生重传后,如果收到确认,无法确定这个确认是针对原始传输还是重传。这被称为重传歧义问题,它会影响RTT的测量和RTO的计算。
解决方法包括:
选择性确认(SACK)机制可以提高重传效率,但它也增加了TCP头部的大小和处理复杂性。在某些资源受限的环境中,这可能是一个问题。
优化方法包括:
TCP的差错控制机制与拥塞控制机制密切相关,它们共同决定了TCP的行为。在某些情况下,这两种机制可能会相互影响,导致性能下降。
优化方法包括:
TCP的差错控制机制是其可靠传输的重要组成部分,它通过多种机制检测和处理传输过程中可能出现的错误,确保了数据的完整性和正确性。理解差错控制的原理和实现,对于优化网络应用和解决网络问题都有重要意义。