Linux:TCP 序列号简介

文章目录

  • 1. 前言
  • 2. 什么是 TCP 序列号?
  • 3. TCP 序号 的 初始值设置 和 后续变化过程
    • 3.1 三次握手 连接建立 期间 客户端 和 服务端 序号 的 变化过程
      • 3.1.1 客户端 socket 初始序号 的 建立
      • 3.1.2 服务端 socket 初始序号 的 建立
      • 3.1.3 客户端 socket 接收 服务端 SACK,然后回复 服务端 ACK
      • 3.1.4 服务端 接收 客户端 ACK,完成 三次握手
      • 3.1.5 三次握手期间的序号变化总结
    • 3.2 连接建立后,数据 通信 过程中 序号 的变化
    • 3.3 四次挥手 连接断开 期间 序号 的变化

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 什么是 TCP 序列号?

TCP 序列号,是为 TCP 协议通信数据中的每一个字节赋予一个唯一编号,其作用可以概括如下:

1. 数据分段(Segment)与重组
   一次发送的数据长度如果超过设定的 MSS(Maximum Segment Size),就会被分成多个带有 TCP 
   协议头的段(Segment)分别发送出去;接收端需将接收的分段(Segment)数据按每个 TCP 数据段
   (Segment)头部中的序列号来进行重组。
2. 数据的可靠性传输
   接收端在收到发送端的数据后,通过 ACK 返回接收数据的序列号,这样发送端就可以确认数据已
   抵达接收端。
3. 流量控制
   TCP 通过滑动窗口来进行流量控制。通信两端各自维护自身的 发送 和 接收滑动窗口大小,并将
   自身接收滑动窗口大小 和 序列号 发送给对端,让对端知道当前最大可以发送的数据量。
4. 防止重复报文
   TCP 有超时重发机制,如果发送的数据超过一定时间没有收到对方的确认ACK,则会认为数据可能
   丢失而进行重发。在有些情形下,数据并没有丢失,只是因为某些原因导致在发送路途中消耗时间
   过长,如果这个时间超过了 TCP 的超时重发时间,则接收端会重复接收到同一数据,这时可通过
   丢弃具有相同序列号的数据来去重。

一图胜千言,还是上一张图来直观感受下:
在这里插入图片描述
上图是 TCP 协议数据头部,红框中的部分:序号 表示发送端所发送数据的序列号确认号 表示接收端已收到发送端序号为确认号之前的所有数据,回送确认号告诉发送端,可以继续发送序号从确认号开始的数据。TCP 序列号以一个 32 位无符号数 表示,最大值为 2^32 - 1,到达最大值后回卷到 0

3. TCP 序号 的 初始值设置 和 后续变化过程

从前面的章节中,我们对 TCP 序号 有了一个初步了解,本文剩余部分将结合图示和内核源码,来说说 连接建立初期 TCP 序号初始值的设定过程、以及 连接建立后 TCP 通信当中序号的变化过程
在正式讨论 TCP 序号 的相关细节前,首先要明确的是,TCP 序号 是基于每 socket 进行维护的,即每个 socket 都有自己独立的序号,从后面的代码分析我们将看到这一点。
本文以典型的 TCP 服务端客户端 通信过程,说明 TCP 序号 的维护细节。本文基于 Linux 4.14 内核源码进行分析。先上两张图,分别描述了 TCP 套接字状态机TCP socket 通信序列号变化过程(包括 三握四挥、数据传输),如下:
在这里插入图片描述
在这里插入图片描述

3.1 三次握手 连接建立 期间 客户端 和 服务端 序号 的 变化过程

TCP 服务端客户端 socket 各自的 序号初始值 在连接建立的 三次握手 过程中建立,分析过程参考上面的第二张图进行(我们假定 服务端 当前处于 LISTEN 态(TCP_LISTEN)(即已经调用了 listen())。

3.1.1 客户端 socket 初始序号 的 建立

客户端 socket 通过 connect() 系统调用,构建 SYN 包服务端 发起连接请求,其 初始序号 建立的主要流程如下:

sys_connect()
	...
	tcp_v4_connect()
		...
		tcp_set_state(sk, TCP_SYN_SENT); /* 标记 客户端 socket 进入 TCP_SYN_SENT 状态 */
		...
		tp->write_seq = secure_tcp_seq(...)/* 初始化 客户端 socket 序号 为 随机值 x (假定 x = 4065008942) */
		...
		tcp_connect(sk);
			struct tcp_sock *tp = tcp_sk(sk); /* 客户端 TCP socket 对象 */
 			struct sk_buff *buff;
 			
			...
			buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true); /* 为 SYN 包分配空间 */
			...
			/*
 			 * 初始化 TCP SYN 包 TCP 控制块(tcp_skb_cb): 
			  * . 设置 SYN 包 初始 序号
			  * . 设置 SYN 包 SYN 标志
			  */
			tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
				TCP_SKB_CB(skb)->tcp_flags = flags; /* 设置 SYN 标志位 */
 				TCP_SKB_CB(skb)->sacked = 0;
 				...
				TCP_SKB_CB(skb)->seq = seq; /* SYN 包 序号 设为 x (x = 4065008942) */
				...
			...
			tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
				__tcp_transmit_skb(sk, skb, clone_it, gfp_mask, tcp_sk(sk)->rcv_nxt);
					struct tcphdr *th; /* TCP 数据包头部 */
					...
					th->seq   = htonl(tcb->seq); /* 客户端 发往 服务端 SYN 包:序号 为 x (x = 4065008942) */
					th->ack_seq  = htonl(rcv_nxt); /* 客户端 发往 服务端 的 SYN 包:确认号 为 0 */
					/* 设置 SYN 标志位 */
					*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) | tcb->tcp_flags);
					...
			...
			tp->snd_nxt = tp->write_seq; /* 更新 套接字 的 发送滑动窗口 下一要发送字节 的 序号 */
			...

从上面的代码分析看到,客户端 socketconnect() 中构建一个 SYN 包,在 SYN 包 构建过程中确定了 初始序号,且设置了 SYN 标志位。此时 客户端 socket 的状态 也由 CLOSED(TCP_CLOSED) 转化为 SYN-SENT(TCP_SYN_SENT) 。用 tcpdump服务端 抓包,结果如下:

00:30:27.921869 IP 169.228.88.168.52524 > 169.228.88.188.8888: Flags [S], seq 4065008942, win 64240, options [mss 1460,sackOK,TS val 3875254422 ecr 0,nop,wscale 7], length 0

可以看到,服务端 169.228.88.188.8888 收到了从 客户端 169.228.88.168.52524 发送的 SYN 包,客户端初始序号x=4065008942tcpdump[S] 标记 SYN 包

3.1.2 服务端 socket 初始序号 的 建立

服务端 收到 客户端 序号xSYN 包 后,会回复 确认号x+1ACK客户端;同时构建一个 序号ySYN 包,向 客户端 发起连接请求。很自然的,服务端回复 ACK请求 SYN 放在同一个 TCP 包里,一同发往 客户端,而不是分开发送,这就是平常所见的 SACK 包,或者 SYN + ACK 包。前述过程的主要代码流程如下:

/* 
 * 从网卡接收数据中断接口开始: 
 * 服务端 收到 客户端 的 SYN 连接请求后,回复 客户端 SYN + ACK.
 */
xxx_nic_interrput()
	...
	ip_rcv()
		...
		tcp_v4_rcv()
			...
			const struct iphdr *iph; /* IP 头部 */
			const struct tcphdr *th; /* TCP 头部 */
			...
			struct sock *sk; /* TCP 服务端 socket 对象 */
			...

			/* 找到 TCP 服务端 socket 对象 */
			th = (const struct tcphdr *)skb->data;
 			iph = ip_hdr(skb);
 		lookup:
			sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
						th->dest, sdif, &refcounted);	
			
			...
			
			 /* 从 客户端 socket 发送的 SYN 包 提取 序号 x (x = 4065008942)、确认号 等(到 TCP 控制块 struct tcp_skb_cb) */
			th = (const struct tcphdr *)skb->data;
			iph = ip_hdr(skb);
			tcp_v4_fill_cb(skb, iph, th);
				TCP_SKB_CB(skb)->seq = ntohl(th->seq); /* TCP_SKB_CB(skb)->seq = x; (x = 4065008942)  */
				...
				TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq); /* 0 */
				...
				TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th); /* SYN */
 				TCP_SKB_CB(skb)->tcp_tw_isn = 0;
 				...
			...
 			if (sk->sk_state == TCP_LISTEN) { /* TCP 服务端 socket 当前处于 LISTEN 状态(TCP_LISTEN) */
 				ret = tcp_v4_do_rcv(sk, skb);
 					...
 					tcp_rcv_state_process(sk, skb))
 						struct tcp_sock *tp = tcp_sk(sk);
 						...
 						const struct tcphdr *th = tcp_hdr(skb);
 						...
 						
 						switch (sk->sk_state) {
 						...
 						case TCP_LISTEN: /* TCP 服务端 socket 当前处于 LISTEN 状态(TCP_LISTEN) */
 							...
 							if (th->syn) { /* 是 客户端 socket 发送的 SYN 包(设置了 SYN 标志位) */
 								...
 								/* 调用 tcp_v4_conn_request(), 见后续分析 */
 								acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
 								...
 								
 								consume_skb(skb); /* 消耗 客户端 socket 的 SYN 包 */
 								return 0;
 							}
 							...
 						...
 						}
 					...
 				...
 			}

/* 接上面的分析 */
acceptable =  tcp_v4_conn_request(sk, skb) >= 0;
	tcp_conn_request(&tcp_request_sock_ops, &tcp_request_sock_ipv4_ops, sk, skb);
		...
		__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn; // 0
		...
		/* 服务端 TCP socket, 在关闭前会一直处于 TCP_LISTEN 态, 通过 accept() 接收客户端的 SYN 连接请求 */
		struct tcp_sock *tp = tcp_sk(sk);
		...
		/* 即将 在 服务端 新建一个 socket 对象, 和 客户端 发起连接 的 socket 通信 */
		struct request_sock *req;
		...

		/*
		 * !!!
		 * 为 客户端 连接请求 分配 轻量 socket 对象 struct request_sock.
		 * 记住 这个 轻量 socket 对象,它将用于 服务端 在接收 客户端 ACK 的处理.
		 */
		req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); /* (1) */
			struct request_sock *req = reqsk_alloc(ops, sk_listener, attach_listener);
				struct request_sock *req;
				
				req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
				...
				if (attach_listener) {
					...
					/* 
					 * 记录 客户端 连接信息 的 轻量 socket 是 向 服务端 socket 对象 @sk_listener
					 * 发起的,即 @req->rsk_listener 记录了 服务端 socket 对象。
					 */
					req->rsk_listener = sk_listener;
				}
				...
			
			if (req) {
				struct inet_request_sock *ireq = inet_rsk(req);
				
				...
				/* 记录 客户端 连接的 轻量 socket 初始为 SYN-RECEIVED(TCP_NEW_SYN_RECV) 态 */
				ireq->ireq_state = TCP_NEW_SYN_RECV;
				write_pnet(&ireq->ireq_net, sock_net(sk_listener));
  				ireq->ireq_family = sk_listener->sk_family;
			}
		...

		tcp_rsk(req)->af_specific = af_ops; /* &tcp_request_sock_ipv4_ops */
		...

		tcp_openreq_init(req, &tmp_opt, skb, sk);
			...
			
			req->rsk_rcv_wnd = 0;
			...
			/* 记录 客户端 的 初始序号 x (x = 4065008942) */
			tcp_rsk(req)->rcv_isn = TCP_SKB_CB(skb)->seq; /* tcp_rsk(req)->rcv_isn = x; */
			/* 服务端 期待 收到 的 下一 客户端 数据的 序号 x + 1 (x + 1 = 4065008942 + 1 = 4065008943) */
			tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; /* tcp_rsk(req)->rcv_nxt = x + 1;  */
			...
		...

		/* 为 服务端 新建的、用来和 客户端 通信的 socket 生成 初始 序号(ISN: Initial Sequence Number) */
		if (!want_cookie && !isn) {
			...
			isn = af_ops->init_seq(skb); /* tcp_v4_init_seq() */
				tcp_v4_init_seq(skb)
					secure_tcp_seq(...)
		}

		/* 设定 服务端 新建的、用来和 客户端 通信 socket 的 初始 序号 y (假定 y = 1093122830) */
		tcp_rsk(req)->snt_isn = isn;
		...
		if (fastopen_sk) {
			...
		} else {
			if (!want_cookie)
				/* 将记录 客户端 连接的 轻量级 的 socket 添加到 服务端 socket 的 SYN 队列(半连接队列) */
				inet_csk_reqsk_queue_hash_add(sk, req, 
						tcp_timeout_init((struct sock *)req));
			/* 服务端 回复 客户端 ACK,同时发送 SYN 连接请求 (tcp_v4_send_synack()) */
			af_ops->send_synack(sk, dst, &fl, req, &foc,
				    	!want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
				tcp_v4_send_synack(sk, dst, &fl, req, &foc, TCP_SYNACK_NORMAL)
					...
					/*
 					 * 构建 SYN + ACK 包: 
 					 * . ACK 是对 客户端 SYN 连接请求的回复;
 					 * . SYN 是 服务端 向 客户端 发起的连接请求.
 					 */
					skb = tcp_make_synack(sk, dst, req, foc, synack_type);
						struct tcphdr *th;
						...
						
						skb = alloc_skb(MAX_TCP_HEADER, GFP_ATOMIC); /* 分配 skb 空间 */
						...
						
						th = (struct tcphdr *)skb->data;
						memset(th, 0, sizeof(struct tcphdr));
						th->syn = 1; /* 设置 SYN 标志位 */
						th->ack = 1; /* 设置 ACK 标志位 */
						...
						/* th->seq = y; (y = 1093122830) */
						th->seq = htonl(tcp_rsk(req)->snt_isn);
						/* th->ack_seq = x + 1; (x + 1 = 4065008942 + 1 = 4065008943) */
						th->ack_seq = htonl(tcp_rsk(req)->rcv_nxt);
						...
						
					if (skb) {
						...
						/* 服务端 回复 客户端 SYN 请求 ACK, 同时向 客户端 发送 连接请求 SYN */
						err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
									ireq->ir_rmt_addr,
									rcu_dereference(ireq->ireq_opt));
						...
					}
		}

3.1.3 客户端 socket 接收 服务端 SACK,然后回复 服务端 ACK

接下来 客户端 socket 收到 服务端 的 SYN + ACK 包,其中 ACK 是 服务端 对 客户端 SYN 的回复,而 SYN 是来自 服务端 的连接请求。从 3.1.1 的分析了解到,客户端套接字当前处于 SYN-SENT(TCP_SYN_SENT) 状态。来看 客户端 收取 服务端 ACK 并 回复 服务端 SYNACK 的代码细节:

/*
 * 从网卡接收数据中断接口开始:
 * 客户端 接收 服务端 对 自身 SYN 请求的 ACK, 并对 服务端 的 SYN 请求 回复 ACK.
 */
xxx_nic_interrput()
	...
	ip_rcv()
		tcp_v4_rcv()
			...
			if (!sock_owned_by_user(sk)) {
				ret = tcp_v4_do_rcv(sk, skb);
					...
					tcp_rcv_state_process(sk, skb)
						...
						switch (sk->sk_state) {
						...
						case TCP_SYN_SENT: /* 客户端 socket 当前处于 SYN-SENT(TCP_SYN_SENT) 态 */
							queued = tcp_rcv_synsent_state_process(sk, skb, th); /* 见后续分析 */
							...
							return 0;
						...
						}
			} else if (tcp_add_backlog(sk, skb)) {
				...
			}
			...

/* 接上面的分析 */
queued = tcp_rcv_synsent_state_process(sk, skb, th);
	...
	if (th->ack) { /* 客户端 收到 服务端 对 SYN 回复的 SYN + ACK */
		...
		
		if (!th->syn) /* 不是 (SYN + ACK) */
   			goto discard_and_undo;
   		
   		...

		/* 客户端 期待的 服务端 的下一数据 序号 y + 1 */
		tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; /* tp->rcv_nxt = y + 1; (y + 1 = 1093122830 + 1 = 1093122831) */
  		tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

		...
		
		/*
		 * 客户端 完成 连接建立工作:
		 * 客户端 socket 由 SYN-SENT(TCP_SYN_SENT) 转为 ESTABLISHED(TCP_ESTABLISHED) 态. 
		 */
		tcp_finish_connect(sk, skb);
			...
			tcp_set_state(sk, TCP_ESTABLISHED); /* 客户端 socket 进入 ESTABLISHED(TCP_ESTABLISHED) 态 */
			...
		...

		if (...) {
			...
		}  else {
			tcp_send_ack(sk); /* 构建 + 向 服务端 回送 ACK 包 */
				__tcp_send_ack(sk, tcp_sk(sk)->rcv_nxt);
					struct sk_buff *buff;
					...
					buff = alloc_skb(MAX_TCP_HEADER, ...);
					...
					tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);
						
					...
					__tcp_transmit_skb(sk, buff, 0, (__force gfp_t)0, rcv_nxt);
						...
						struct tcp_skb_cb *tcb;
						...
						struct tcphdr *th;
						...
						
						tcb = TCP_SKB_CB(skb);
						...
						th = (struct tcphdr *)skb->data;
						...
						/* th->seq = x + 1; (x + 1 = 4065008942 + 1 = 4065008943) */
						th->seq   = htonl(tcb->seq);
						/* th->ack_seq = y + 1; (y + 1 = 1093122830 + 1 = 1093122831) */
 						th->ack_seq  = htonl(rcv_nxt); 
 						/* 设置 TCP 头部的 ACK 标记 */
 						*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) | 
 										tcb->tcp_flags);
 						...
 						/* 将 ACK 向下传递给 网络层 发送出去 */
 						err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); /* ip_queue_xmit() */
		}
		...
	}

至此,客户端 ==> 服务端 的 单边连接 已经建立,客户端 socket 已经进入 ESTABLISHED(TCP_ESTABLISHED) 状态。用 tcpdump服务端 抓包,结果如下:

00:30:28.005237 IP 169.228.88.188.8888 > 169.228.88.168.52524: Flags [S.], seq 1093122830, ack 4065008943, win 65160, options [mss 1460,sackOK,TS val 3225418910 ecr 3875254422,nop,wscale 5], length 0

可以看到,服务端 169.228.88.188.8888客户端 169.228.88.168.52524 回复了 一个 SACK 包(SYN + ACK)服务端初始序号y=1093122830确认号x+1=4065008943tcpdump[S.] 标记 SACK 包。

3.1.4 服务端 接收 客户端 ACK,完成 三次握手

从上一小节 3.1.3 了解到,客户端 ==> 服务端 的 单边连接 已经建立,但 服务端 ==> 客户端 的 单边连接 尚未得到确认(即 服务端 还没有收取 客户端 对 SYNACK 回复)。下面来看 服务端 接收 客户端 ACK 完成 三次握手 的代码细节:

xxx_nic_interrput()
	...
	ip_rcv()
		tcp_v4_rcv()
			...
			const struct iphdr *iph;
 			const struct tcphdr *th;
 			...

			...
			th = (const struct tcphdr *)skb->data;
			iph = ip_hdr(skb);
		lookup:
			/* 
			 * !!! 
			 * 注意,这里查找到的 socket 对象,不再是 服务端 socket 套接字,
			 * 而是 服务端 收到 客户端 SYN 时 新建的、记录了 客户端连接信息
			 * 的 轻量 socket 对象 struct request_sock ,即 3.1.2 代码分
			 * 析注释中、 标记 (1) 处建立的 socket 对象。
			 */
			sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
					th->dest, sdif, &refcounted);
			...

			if (sk->sk_state == TCP_NEW_SYN_RECV) {
				struct request_sock *req = inet_reqsk(sk); /* 服务端 记录 客户端连接信息的 轻量 socket */

				sk = req->rsk_listener; /* sk = 服务端 监听 socket */
				...
				if (!tcp_filter(sk, skb)) {
					th = (const struct tcphdr *)skb->data;
					iph = ip_hdr(skb);
					tcp_v4_fill_cb(skb, iph, th);
						/* TCP_SKB_CB(skb)->seq = x + 1; (x + 1 = 4065008942 + 1 = 4065008943)  */
						TCP_SKB_CB(skb)->seq = ntohl(th->seq);
						/* TCP_SKB_CB(skb)->ack_seq = y + 1; (y + 1 = 1093122830 + 1 = 1093122831) */
						TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
						...
						TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th); /* ACK */
						TCP_SKB_CB(skb)->tcp_tw_isn = 0;
						...
					...
				}
				...
			}

至此,客户端 <==> 服务端 的双向连接 已经建立完成。用 tcpdump服务端 抓包,结果如下:

00:30:28.006550 IP 169.228.88.168.52524 > 169.228.88.188.8888: Flags [.], ack 1093122831, win 502, options [nop,nop,TS val 3875254506 ecr 3225418910], length 0

可以看到,服务端 169.228.88.188.8888 收到 客户端 169.228.88.168.52524 发送的 ACK确认号y+1 = 1093122831tcpdump[.] 标识 ACK

3.1.5 三次握手期间的序号变化总结

下面描述中的 seq 表示 序号ack_req 表示 确认号flags 表示 TCP 协议头部标记(SYN,ACK 等)

1. 客户端 向 服务端 发起 SYN 连接请求,SYN 包的 TCP 头部中: seq=x, ack_req=0, flags=SYN;
2. 服务端 收到 客户端 的 SYN 后,回复 客户端 SACK 包(SYN+ACK),SACK 包的 TCP 头部中: seq=y, ack_req=x+1, flags=SYN|ACK;
3. 客户端 收到 的 SACK 后,回复 服务端 一个 ACK 包,ACK 包的 TCP 头部中:seq=x+1, ack_req=y+1 。

可以看到,通信两端(服务端 和 客户端)建立 初始序号 后,在 三次握手 期间,各自的 SYN 消耗了 1 个序号,最终,通信两端 的 序号 分别 停在了 x+1y+1 上。

3.2 连接建立后,数据 通信 过程中 序号 的变化

在连接建立后,通信两端分别在当前序号 x+1y+1 的基础上,每次以发送数据长度递增序号,发往对端数据的 确认号 为 接收到的对端数据中 序号 加 1。看一下 tcpdump 抓包情况:

00:30:57.585899 IP 169.228.88.168.52524 > 169.228.88.188.8888: Flags [P.], seq 4065008943:4065009967, ack 1093122831, win 502, options [nop,nop,TS val 3875284086 ecr 3225418910], length 1024
00:30:57.605209 IP 169.228.88.188.8888 > 169.228.88.168.52524: Flags [.], ack 4065009967, win 2005, options [nop,nop,TS val 3225448498 ecr 3875284086], length 0

可以看到,客户端 169.228.88.168.52524服务端 169.228.88.188.8888 ,发送了 1024 个字节,数据 序号 位于 半开半闭 区间 [x+1=4065008943, x+1024=4065009967) (不包括 4065009967);而 服务端 169.228.88.188.8888 确认收到 客户端 169.228.88.168.52524 的所有 1024 个字节后,回复 客户端 169.228.88.168.52524 一个 ACK,其 确认号为 x+1024=4065009967

3.3 四次挥手 连接断开 期间 序号 的变化

本文不对 四次挥手 期间 序号 的变化过程进行分析,感兴趣的读者可参考博文 Linux:TCP三握四挥简析,自行阅读源码分析。看一下 tcpdump 抓包情况:

00:30:57.605252 IP 169.228.88.168.52524 > 169.228.88.188.8888: Flags [F.], seq 4065009967, ack 1093122831, win 502, options [nop,nop,TS val 3875284086 ecr 3225418910], length 0
00:30:57.669707 IP 169.228.88.188.8888 > 169.228.88.168.52524: Flags [.], ack 4065009968, win 2005, options [nop,nop,TS val 3225448552 ecr 3875284086], length 0
00:31:02.468946 IP 169.228.88.188.8888 > 169.228.88.168.52524: Flags [F.], seq 1093122831, ack 4065009968, win 2005, options [nop,nop,TS val 3225453368 ecr 3875284086], length 0
00:31:02.469779 IP 169.228.88.168.52524 > 169.228.88.188.8888: Flags [.], ack 1093122832, win 502, options [nop,nop,TS val 3875288970 ecr 3225453368], length 0

可以看到:

1. `客户端 169.228.88.168.52524` 主动发起关闭(调用  `close()`),向 `服务端 169.228.88.188.8888` 发送 `序号``x+1024=4065009967``确认号``y+1=1093122831``FIN` 包。`客户端` 发送了 1024 个字节后,再没有数据发送,
   所以 `FIN` 包 序号 紧接数据序号 之后;同时由于 `服务端` 没有 发送数据,所以 `确认号` 没有变化。
2. `服务端 169.228.88.188.8888` 回复 `客户端 169.228.88.168.52524``FIN` 包 一个 `ACK``确认号``x+1024+1=4065009968`。至此,`客户端 到 服务端 的单向连接 已经断开`3. `服务端 169.228.88.188.8888``客户端 169.228.88.168.52524` 发送 FIN 包,请求断开连接,该包的 `序号` 为
   y+1=1093122831,确认号 为 `x+1024+1=4065009968`,因为 客户端 到 服务端 的单向连接 已经断开,不会再有从 客户端
   到 服务端 的数据,所以 确认号 不再变换。
4. `客户端 169.228.88.168.52524` 回复 `服务端 169.228.88.188.8888``FIN` 包 一个 `ACK``确认号``y+1+1=1093122832`。至此,`服务端 到 客户端 的单向连接 已经断开`,于是整个连接彻底断开。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/244668.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

平台工程与 DevOps 和 SRE 有何不同?

在现代软件开发和运营的动态领域中 &#xff0c;平台工程、DevOps 和站点可靠性工程 (SRE) 等术语 经常使用&#xff0c;有时可以互换使用&#xff0c;这常常会导致进入或浏览这些领域的专业人员感到困惑。了解这些概念之间的细微差别对于努力构建强大且可扩展的系统的组织至关…

Vue3-12- 【v-for】循环一个整数

说明 v-for 这个东西就很神奇&#xff0c;可以直接循环一个整数&#xff0c;而且循环的初始值是从1 开始。使用案例 <template><div v-for"(num,indexB) in 6" :key"indexB">【索引 {{ indexB }}】 - 【数字 {{ num }}】 </div></t…

ArkTS入门

代码结构分析 struct Index{ } 「自定义组件&#xff1a;可复用的UI单元」 xxx 「装饰器&#xff1a;用来装饰类结构、方法、变量」 Entry 标记当前组件是入口组件&#xff08;该组件可被独立访问&#xff0c;通俗来讲&#xff1a;它自己就是一个页面&#xff09;Component 用…

影响云渲染质量的几大要素是什么?影响云渲染质量的主要原因有?

对于3D渲染从业者而言&#xff0c;实现高效和高质量的渲染是一个常见的挑战。由于三维场景的复杂性&#xff0c;相关计算和处理通常需要大量的计算能力和存储&#xff0c;尤其是当面对着高分辨率图像、详细的动画或全局光照效果等要求时&#xff0c;渲染时间往往会大幅增加。针…

Vue 详细教程

Vue实战 1. Vue 引言 渐进式 JavaScript 框架 --摘自官网 官网地址&#xff1a;Vue.js - 渐进式 JavaScript 框架 | Vue.js # 渐进式 1. 易用 html css javascript 2. 高效 开发前端页面 非常高效 3. 灵活 开发灵活 多样性 # 总结 Vue 是一个javascript 框架 js 简化页面js操作…

数据挖掘-07-航空公司客户价值分析(包括数据和代码)

文章目录 0. 数据代码下载1. 背景与挖掘目标2. 导入相关库&#xff0c;加载数据2.1客户基本信息分布a. 绘制会员性别比例饼图b. 绘制会员各级别人数条形图c. 绘制年龄分布图 2.2 客户乘机信息分布分析a. 绘制客户飞行次数箱线图b. 绘制客户总飞行公里数箱线图 2.3 客户积分信息…

【二叉树相关问题】

文章目录 一、二叉树的三种遍历方式怎么看遍历结果相关题目&#xff1a;已知一颗二叉树的后续遍历序列为&#xff1a;GFEDCBA;中序遍历序列为&#xff1a;FGAEBDC。画出这棵二叉树思路代码版 二、先序线索树三、二叉树转树、或森林树转二叉树二叉树转树二叉树转森林森林转二叉树…

解析硬盘备份与云备份的差异

​  在数字信息时代&#xff0c;保护您的数据至关重要。外部硬盘驱动器 (HDD) 备份和云备份算是两种流行的数据备份方法。当然&#xff0c;每种方法都有其优点和考虑因素&#xff0c;选择正确的解决方案取决于您的具体需求和偏好。 一、外部硬盘备份 传统的数据备份方法之一是…

Java刷题篇——LeetCode118. 杨辉三角

1.题目描述 给定一个非负整数numRows&#xff0c;生成杨辉三角的前numRows行。 在杨辉三角中&#xff0c;每个数是它左上方和右上方的数的和。 示例1 输入&#xff1a;numRows 5 输出&#xff1a;[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1] 示例2 输入&#xff1a;numRows 1…

基于CNN+数据增强+残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)+数据集+模型(二)

系列文章目录 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xff08;一&#xff09; 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xf…

【Java8系列08】Java8中reducing妙用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

提升团队效率,防止员工飞单私单的秘诀!

在如今竞争激烈的商业环境中&#xff0c;每个企业都追求更高的销售业绩和客户满意度。然而&#xff0c;有些员工可能会利用V信等社交工具进行私下交易&#xff0c;导致公司的利益损失和客户信任的瓦解。所以&#xff0c;如何防止员工飞单私单成为了一个需要解决的问题。 在这里…

喜报丨迪捷软件入选2023年浙江省信息技术应用创新典型案例

12月6日&#xff0c;浙江省经信厅公示了2023年浙江省信息技术应用创新典型案例入围名单。本次案例征集活动&#xff0c;由浙江省经信厅、省密码管理局、工业和信息化部网络安全产业发展中心联合组织开展&#xff0c;共遴选出24个优秀典型解决方案&#xff0c;迪捷软件“基于全数…

单例模式:饿汉模式、懒汉模式

目录 一、什么是单例模式 二、饿汉模式 三、懒汉模式 一、什么是单例模式 单例模式是Java中的设计模式之一&#xff0c;能够保证某个类在程序中只存在唯一一份实例&#xff0c;而不会创建出多个实例 单例模式有很多实现方式&#xff0c;最常见的是饿汉和懒汉两种模式 二、…

KNN朴素贝叶斯(根据已知推测未知)

KNN&#xff08;哲学思想&#xff1a;物以类聚&#xff0c;人以群分&#xff09; KNN算法原理及示例1&#xff1a; 向量化 画点&#xff0c;计算欧式距离&#xff1a; 可行代码展示&#xff1a; #!/usr/bin/python # codingutf-8 ######################################### …

如何在 VeriStand 中设置反射内存通道

环境 硬件 cPCI-5565PIORC 软件 VeriStand 我正在设置我的反射内存 PXI 卡&#xff08;例如 cPCI-5565PIORC&#xff09;。 我可以在我的 PXI 系统之间使用反射内存发送/接收什么&#xff1f; 如何设置我的 PXI 系统之间共享的通道&#xff1f; 使用反射内存&#xff0c;您…

国际语音呼叫中心的工作流程

国际语音呼叫中心的工作流程一般包括以下几个步骤&#xff1a; 1.呼叫分配 当客户拨打企业的客服电话时&#xff0c;国际语音呼叫中心会自动将呼叫分配给示闲的客服代表&#xff0c;或者根据客户的需求&#xff0c;将呼叫转接给相应的客服代表。 2.客服代表接听电话 客服代…

网络小测------

使用软件PT7.0按照上面的拓扑结构建立网络&#xff0c;进行合理配置&#xff0c;使得所有计算机之间能够互相通信。并且修改各交换机的系统名称为&#xff1a;学号_编号&#xff0c;如你的学号为123&#xff0c;交换机Switch0的编号为0&#xff0c;则系统名称为123_0&#xff1…

史上最全的设计模式总结

从七月份开始一直到九月底才看完设计模式&#xff0c;在这个过程中我不敢说我已经掌握了那本书里面的内容&#xff0c;或者说1/5&#xff0c;没能力说也没有资格说。但是结果不重要&#xff0c;重要的是这个过程我的收获&#xff01;主要包括如下几个方面&#xff1a; 1、认识了…

华为OD机试 - 任务最优调度 - 深度优先搜索dfs算法(Java 2023 B卷 200分)

目录 专栏导读一、题目描述二、输入描述三、输出描述1、输入2、输出3、说明 四、解题思路1、题目解读2、解题思路3、具体步骤 五、Java算法源码六、效果展示1、输入2、输出3、说明思路分析执行顺序 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收…