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; /* 更新 套接字 的 发送滑动窗口 下一要发送字节 的 序号 */...

从上面的代码分析看到,客户端 socket 在 connect() 中构建一个 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=4065008942 。tcpdump 用 [S] 标记 SYN 包。

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

服务端 收到 客户端 序号 为 x 的 SYN 包 后,会回复 确认号 为 x+1 的 ACK 给 客户端;同时构建一个 序号 为 y 的 SYN 包,向 客户端 发起连接请求。很自然的,服务端 将 回复 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 并 回复 服务端 SYN 的 ACK 的代码细节:

/** 从网卡接收数据中断接口开始:* 客户端 接收 服务端 对 自身 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=4065008943 。tcpdump 用 [S.] 标记 SACK 包。

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

从上一小节 3.1.3 了解到,客户端 ==> 服务端 的 单边连接 已经建立,但 服务端 ==> 客户端 的 单边连接 尚未得到确认(即 服务端 还没有收取 客户端 对 SYN 的 ACK 回复)。下面来看 服务端 接收 客户端 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 = 1093122831 。tcpdump 用 [.] 标识 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+1 和 y+1 上。

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

在连接建立后,通信两端分别在当前序号 x+1 和 y+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`。至此,`服务端 到 客户端 的单向连接 已经断开`,于是整个连接彻底断开。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue使用el-tag完成添加标签操作

需求&#xff1a;做一个添加标签的功能&#xff0c;点击添加后输入内容后回车可以添加&#xff0c;并且标签可以删除 1.效果 2.主要代码讲解 鼠标按下后触发handleLabel函数&#xff0c;根据回车的keycode判断用户是不是按下的回车键&#xff0c;回车键键值为13&#xff0c;用…

【一种用opencv实现高斯曲线拟合的方法】

背景&#xff1a; 项目中需要实现数据的高斯拟合&#xff0c;进而提取数据中标准差&#xff0c;手头只有opencv库&#xff0c;经过资料查找验证&#xff0c;总结该方法。 基础知识&#xff1a; 1、opencv中solve可以实现对矩阵参数的求解&#xff1b; 2、线的拟合就是对多项…

leetcode:457. 环形数组是否存在循环

环形数组是否存在循环 存在一个不含 0 的 环形 数组 nums &#xff0c;每个 nums[i] 都表示位于下标 i 的角色应该向前或向后移动的下标个数&#xff1a; 如果 nums[i] 是正数&#xff0c;向前&#xff08;下标递增方向&#xff09;移动 |nums[i]| 步 如果 nums[i] 是负数&…

【23-24 秋学期】NNDL 作业11 LSTM

目录 习题6-4 推导LSTM网络中参数的梯度&#xff0c; 并分析其避免梯度消失的效果 习题6-3P 编程实现下图LSTM运行过程 &#xff08;一&#xff09;numpy实现 &#xff08;二&#xff09;使用nn.LSTMCell实现 &#xff08;三&#xff09; 使用nn.LSTM实现 总结 &#x…

dysmsapi

dysmsapi DY - SMS - API 短信服务接口 短信服务_SDK中心-阿里云OpenAPI开发者门户 <!-- 阿里dayu sms api短信群发接口 --><!-- https://mvnrepository.com/artifact/com.aliyun/dysmsapi20170525/2.0.24 --><dependency><groupId>com.aliyun&l…

Python学习笔记第七十四天(OpenCV安装)

Python学习笔记第七十四天 OpenCV安装安装Python安装OpenCV简单使用OpenCV 后记 OpenCV安装 在Windows系统下&#xff0c;安装Python和OpenCV可以按照以下步骤进行&#xff1a; 安装Python 下载Python&#xff1a;在Python官网下载最新的Python安装包&#xff0c;建议选择与…

JS匿名函数之函数表达式与立即执行函数

匿名函数是什么&#xff1f;和具名函数有什么区别&#xff1f;让我为大家介绍一下吧&#xff01; 没有名字的函数&#xff0c;无法直接使用 一.函数表达式 将匿名函数赋值给一个变量&#xff0c;并且通过变量名去调用&#xff0c;我们将这个称为函数表达式 语法&#xff1a; …

Java EE 网络之网络初识

文章目录 1. 网络发展史1.1 独立模式1.2 网络互连1.3 局域网 LAN1.4 广域网 WAN 2. 网络通信基础2.1 IP 地址2.2 端口号2.3 认识协议2.4 五元组2.5 协议分层2.5.1 什么是协议分层2.5.2 分层的作用2.5.3 OSI七层协议2.5.4 TCP/IP五层协议2.5.5 网络设备所在分层 2.6 分装和分用 …

Leetcode的AC指南 —— 链表:24. 两两交换链表中的节点

摘要&#xff1a; Leetcode的AC指南 —— 链表&#xff1a;24. 两两交换链表中的节点。题目介绍&#xff1a;给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能…

【OpenHarmony 北向应用开发】ArkTS语言入门(构建应用页面)

ArkTS语言入门 在学习ArkTS语言之前&#xff0c;我们首先需要一个能够编译并运行该语言的工具 DevEco Studio。 了解ArkTS ArkTS是OpenHarmony优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&#xff0c;继…

有两个循环单链表,链表头指针分别为 h1 和 h2,编写一个函数将 h2 链接到 链表h1 之后,要求处理完仍是一个循环单链表。

题目描述 &#xff1a;有两个循环单链表&#xff0c;链表头指针分别为 h1 和 h2&#xff0c;编写一个函数将 h2 链接到 链表h1 之后&#xff0c;要求处理完仍是一个循环单链表。 分析&#xff1a; 注意题目说的是头指针 h1 和 h2&#xff0c;所以这两个循环单链表并没有头结点…

【MyBatis-Plus】常用的插件介绍(乐观锁、逻辑删除、分页)

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于MyBatis-Plus的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.为什么要使用MyBatis-Plus中的插…

c++程序设计定义一个CForm窗体类,在该类中包括:

定义一个CForm窗体类&#xff0c;在该类中包括&#xff1a; &#xff08;1&#xff09;成员变量&#xff1a;title&#xff08;窗体标题&#xff09;&#xff0c;width&#xff08;窗体宽度&#xff09;&#xff0c;height&#xff08;窗体高度&#xff09;&#xff1b; &…

MySQL中union和union all的区别

一、区别1&#xff1a;取结果的并集 1、union: 对两个结果集进行并集操作, 不包括重复行,相当于distinct, 同时进行默认规则的排序; 2、union all: 对两个结果集进行并集操作, 包括重复行, 即所有的结果全部显示, 不管是不是重复; 二、区别2&#xff1a;获取结果后的操作 1…

【C语言】随机数生成详解,手把手教你,保姆级!!!

目录 rand函数 srand函数 time函数 设置随机数范围 拓展--猜数字游戏 总结 rand函数 C语⾔提供了⼀个函数叫 rand&#xff0c;这函数是可以⽣成随机数的&#xff0c;函数原型如下所⽰ int rand (void); rand函数会返回⼀个伪随机数&#xff0c;这个随机数的范围是在0~RAN…

M个苹果放入N个盘子(递归)

题目&#xff1a; 把M个同样的苹果放在N个同样的盘子里&#xff0c;允许有的盘子空着不放&#xff0c;问共有多少种不同的分法&#xff1f;&#xff08;5&#xff0c;1&#xff0c;1和1&#xff0c;5&#xff0c;1 是同一种分法&#xff09; 输入 每个用例包含二个整数M和N。…

【golang】go执行shell命令行的方法( exec.Command )

所需包: import "os/exec" cmd的用法: cmd : exec.Command("ls", "-lah") //ls是命令,后面是参数 e : cmd.Run() 多个参数的要分开传入: 如:ip link show bond0 cmd :exec.Command("ip","link","show","…

Linux(操作系统)面经——part 1(持续更新中......)

1、说一说常用的 Linux 命令 mkdir创建文件夹&#xff0c;touch创建文件&#xff0c;mv移动文件内容或改名 rm-r 文件名&#xff1a;删除文件 cp拷贝&#xff1a;cp 文件1 文件2&#xff0c;cp-r跨目录拷贝 cp-r 路径1 路径2 vi 插入 &#xff1a;wqb保存退出 :q!强制退出…

【Axure教程】区间评分条

区间评分条是一种图形化的表示工具&#xff0c;用于展示某一范围内的数值或分数&#xff0c;并将其划分成不同的区间。这种评分条通常用于直观地显示数据的分布或某个指标的表现。常用于产品评价、调查和反馈、学术评价、健康评估、绩效评估、满意度调查等场景。 所以今天作者…

DOM树和DOM对象与JS关系的深入研究

const和let使用说明 var不好用&#xff0c;我们如果用变量都是用let&#xff0c;如果用常量乃是不变的量&#xff0c;我们用const&#xff0c;见let const知变量是否可变。比如一个常量在整个程序不会变&#xff0c;但是你用let&#xff0c;是可以的。但是let最好与内部变量改…