当前位置: 首页 > news >正文

Linux:TCP三握四挥简析

文章目录

  • 1. 前言
  • 2. 背景
  • 3. TCP连接的建立和断开
    • 3.1 TCP协议状态机
    • 3.2 TCP的三握四挥
      • 3.2.1 TCP 连接建立的三次握手过程分析
        • 3.2.1.1 服务端和客户端套接字的创建
        • 3.2.1.2 服务端进入 LISTEN 状态
        • 3.2.1.3 服务端在 LISTEN 状态等待客户端的 SYN 请求
        • 3.2.1.4 客户端向服务端发送 SYN 请求建立连接
        • 3.2.1.5 TCP 连接建立过程小结
      • 3.2.2 TCP 连接断开的四次挥手过程分析
        • 3.2.2.1 客户端通过 close() 向服务端发送 FIN,进入 FIN-WAIT-1 状态
        • 3.2.2.2 服务端收取客户端 FIN,回以 ACK,进入 CLOSE-WAIT 状态
        • 3.2.2.3 客户端收取服务端对 FIN 的回应 ACK,进入 FIN-WAIT-2 状态
        • 3.2.2.4 服务端通过 close() 向客户端发 FIN,进入 LAST-ACK 状态
        • 3.2.2.5 客户端收取服务端 FIN,回以 ACK,进入 TIME-WAIT,超时后进入 CLOSED 终态
        • 3.2.2.6 服务端收取客户端对 FIN 的回应 ACK,进入 CLOSED 终态
      • 3.2.3 三握四挥小结
  • 4. 抓包三握四挥过程示例
  • 5. 参考资料

1. 前言

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

2. 背景

本文基于 linux-4.14.132 内核代码进行分析。

3. TCP连接的建立和断开

3.1 TCP协议状态机

在这里插入图片描述

3.2 TCP的三握四挥

在这里插入图片描述
在后面的分析中,我们将始终参考 3.1,3.2 两小节中的状态图。

3.2.1 TCP 连接建立的三次握手过程分析

3.2.1.1 服务端和客户端套接字的创建

通过 socket() 系统调用创建套接字后,套接字初始状态为 CLOSED 状态(即 TCP_CLOSE)。来看代码实现细节:

/* 应用层 通过系统调用 sys_socket() 创建套接字 */
server_fd = socket(AF_INET, SOCK_STREAM, 0); // 服务端 remote_fd = socket(AF_INET, SOCK_STREAM, 0); // 客户端/* 内核空间:初始创建时,套接字为 CLOSED 状态(即 TCP_CLOSE) */
sys_socket(AF_INET, SOCK_STREAM, 0) // net/socket.csock_create(family, type, protocol, &sock)__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);sock = sock_alloc();sock->type = type; // sock->type = SOCK_STREAM;...pf = rcu_dereference(net_families[family]); // 获取协议簇接口...// 进入协议簇(family)的套接字创建过程pf->create(net, sock, protocol, kern) = inet_create() // net/ipv4/af_inet.cstruct sock *sk;sock->state = SS_UNCONNECTED; /* socket 初始为[未连接状态 (SS_UNCONNECTED)] */...list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {if (protocol == answer->protocol) { /* 显式指定了 protocol */...}  else { /* 非显式指定 protocol *//* Check for the two wild cases. */if (IPPROTO_IP == protocol) { /* protocol == 0 意味着创建各 @type 下缺省协议的套接字 */protocol = answer->protocol;break;}...}...}.../* 设定套接字对应协议接口 */sock->ops = answer->ops; /* 设定套接字对应协议接口: &inet_stream_ops */answer_prot = answer->prot; /* &tcp_prot */.../* 创建套接字[网络层管理数据]对象 */sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);struct sock *sk;sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);if (sk) {sk->sk_family = family; // sk->sk_family = PF_INET;sk->sk_prot = sk->sk_prot_creator = prot; /* &tcp_prot */...}return sk;.../* 初始化套接字[网络层管理数据]: 如 type, 状态等 (TCP_CLOSE) */sock_init_data(sock, sk);...//sk->sk_rcvbuf  = sysctl_rmem_default;//sk->sk_sndbuf  = sysctl_wmem_default;//sk->sk_state  = TCP_CLOSE; /* 设定套接字初始状态为 CLOSE */sk_set_socket(sk, sock); /* 绑定网络层管理数据到 socket */sk_tx_queue_clear(sk);sk->sk_socket = sock;...if (sock) {sk->sk_type = sock->type; // sk->sk_type = SOCK_STREAM;sk->sk_wq = sock->wq;sock->sk = sk; /* 设定套接字的网络层管理数据对象 */...}...sk->sk_state_change = sock_def_wakeup;sk->sk_data_ready = sock_def_readable;//sk->sk_write_space = sock_def_write_space;...sk->sk_destruct    = inet_sock_destruct;sk->sk_protocol    = protocol; // IPPROTO_TCPsk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;.../** 前面完成的是 IPv4 协议簇套接字公共初始化。* 这里是 IPv4 协议簇下的子类型 @type 和 子协议 @protocol 套接字的特定初始化。* 套接字的初始化是一个个层层递进的过程,有点类似于 C++ 子类对象的构建过程:* 先调用父类的构造函数,然后在逐级调用子类的构造函数。** 这里是 TCP 类型(IPPROTO_TCP) 套接字 初始化。*/if (sk->sk_prot->init) {err = sk->sk_prot->init(sk); /* tcp_v4_init_sock() */...}.../* TCP 类型套接字初始化 */
tcp_v4_init_sock() // net/ipv4/tcp_ipv4.cstruct inet_connection_sock *icsk = inet_csk(sk);/* TCP 套接字初始化 */tcp_init_sock(sk);...sk->sk_state = TCP_CLOSE; /* TCP 套接字创建时初始状态为 TCP_CLOSE */sk->sk_write_space = sk_stream_write_space;...sk->sk_sndbuf = sysctl_tcp_wmem[1];sk->sk_rcvbuf = sysctl_tcp_rmem[1];.../* 设定 IPv4 TCP 套接字操作接口 */icsk->icsk_af_ops = &ipv4_specific;
3.2.1.2 服务端进入 LISTEN 状态

服务端调用 listen() 后,服务端套接字 由 CLOSED 状态进入 LISTEN :CLOSED => LISTEN

// 用户空间struct sockaddr_in server_addr;
int backlog = 8;memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 服务端 端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.123"); // 服务端 IP 地址
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));listen(server_fd, backlog); // 服务端: CLOSED => LISTEN
// 内核空间sys_listen(fd, backlog) // net/socket.cstruct socket *sock;sock = sockfd_lookup_light(fd, &err, &fput_needed);if (sock) {...err = sock->ops->listen(sock, backlog); /* inet_listen() */...}inet_listen() // net/ipv4/af_inet.cstruct sock *sk = sock->sk;unsigned char old_state;err = -EINVAL;// 处于未连接状态的、 SOCK_STREAM 类型套接字 才能监听if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)goto out;old_state = sk->sk_state;/* 只有对 TCP_CLOSE 或 TCP_LISTEN 态套接字 listen 才是合法的 */if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))goto out;if (old_state != TCP_LISTEN) {.../** . accept 队列初始化* . backlog 初始化* . 套接字由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)*/err = inet_csk_listen_start(sk, backlog);if (err)goto out;}sk->sk_max_ack_backlog = backlog;err = 0;out:return err;inet_csk_listen_start() // net/ipv4/inet_connection_sock.cstruct inet_connection_sock *icsk = inet_csk(sk);reqsk_queue_alloc(&icsk->icsk_accept_queue); // 创建和初始化 accept 队列sk->sk_max_ack_backlog = backlog;sk->sk_ack_backlog = 0;inet_csk_delack_init(sk);sk_state_store(sk, TCP_LISTEN); // 由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)smp_store_release(&sk->sk_state, newstate);...
3.2.1.3 服务端在 LISTEN 状态等待客户端的 SYN 请求
// 服务端的套接字 @server_fd 在 LISTEN 状态等待客户端的 SYN 连接请求
client_fd = accept(server_fd, NULL, NULL);sys_accept(server_fd, NULL, NULL) // net/socket.csys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);struct socket *sock, *newsock;sock = sockfd_lookup_light(fd, &err, &fput_needed);/* 为可能连接的客户端准备一个 sock 对象 */newsock = sock_alloc();...newsock->type = sock->type; // SOCK_STREAMnewsock->ops = sock->ops; // &inet_stream_ops.../** 为客户端 sock 分配一个 fd 。* * !!!注意:* 这和用来监听的套接字 @server_fd 不是同一个,这个是用来管理新连接的* 客户端套接字,也即前面代码中 accept() 返回的 @client_fd 。*/newfd = get_unused_fd_flags(flags);/* 为客户端 sock 分配一个文件对象 (struct file) */newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);...// 等待客户端的 SYN 请求err = sock->ops->accept(sock, newsock, sock->file->f_flags, false); /* inet_accept() *//* 连接客户端的 fd 放入进程的 fd 表 */fd_install(newfd, newfile);err = newfd; /* 返回客户端句柄到用户空间 */...out:return err;inet_accept() // net/ipv4/af_inet.cstruct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern); /* inet_csk_accept() */inet_csk_accept() // net/ipv4/inet_connection_sock.cstruct inet_connection_sock *icsk = inet_csk(sk);struct request_sock_queue *queue = &icsk->icsk_accept_queue;struct request_sock *req;struct sock *newsk;/* Find already established connection */if (reqsk_queue_empty(queue)) {long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); // 这里只讨论阻塞方式,不关心非阻塞方式的逻辑/* If this is a non blocking socket don't sleep */error = -EAGAIN;if (!timeo)goto out_err;// 等待连接请求 SYN 数据包error = inet_csk_wait_for_connect(sk, timeo);if (error)goto out_err;}req = reqsk_queue_remove(queue, sk); // 从 accpet 队列(全连接队列)中取出/移除一个建立好的连接newsk = req->sk; // 返回新建立的客户端连接......newsock->state = SS_CONNECTED; // 等待到客户端连接后,套接字标记为已连接状态 SS_CONNECTED// 等待连接请求 SYN 数据包
inet_csk_wait_for_connect() // net/ipv4/inet_connection_sock.cstruct inet_connection_sock *icsk = inet_csk(sk);DEFINE_WAIT(wait);int err;for (;;) {prepare_to_wait_exclusive(sk_sleep(sk), &wait,TASK_INTERRUPTIBLE);...if (reqsk_queue_empty(&icsk->icsk_accept_queue)) // 没有准备好的连接,陷入睡眠等待客户端 SYN 连接请求进来timeo = schedule_timeout(timeo);...if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) // 有准备好的连接了,结束等待break;...if (signal_pending(current)) // 被信号中断break;err = -EAGAIN;if (!timeo)break;}finish_wait(sk_sleep(sk), &wait);return err;
3.2.1.4 客户端向服务端发送 SYN 请求建立连接

到目前为止,服务端处于 LISTEN 状态,客户端处于 CLOSED 状态。接下来,客户端通过 connect() 调用,向服务端发送 SYN 请求,然后自身进入到 SYN-SENT 状态等待服务端对 SYN 请求SYN + ACK 回复,客户端在收到服务端的 SYN + ACK 回复后,也对服务端的 SYN 回复一个 ACK,之后自身进入到 ESTABLISHED 状态,并从 connect() 调用返回;在 accept() 中等待、处于 LISTEN 状态的服务端套接字(前面代码中的 server_fd 指代的套接字)收到客户端的 SYN 请求后,回复 SYN + ACK 给客户端,之后用刚进入 accept() 时、为新连接的客户端准备的套接字(注意,这是个新的套接字,和用来监听的套接字 server_fd 不是同一个,详见 3.2.1.3 的分析),管理新的客户端连接,并将该套接字状态置为 ESTABLISHED ,最终返回(从 accept() 返回)新套接字的句柄(即 accept() 的返回值)给用户空间使用。细心的读者注意到了吧,用来监听的服务端套接字 server_fd 的状态不会发生变化,仍然处于 LISTEN 状态,服务端处于 ESTABLISHED 状态的套接字,是 accept() 返回的、用来管理客户端新连接的套接字,这从 3.1,3.2 小节中的状态转换图是看不出来的,而且还会误解是监听套接字 server_fd 的状态发生了变化。
上面用文字描述了从客户端发 SYN 包开始,直到最终整个连接的建立过程,下面来看代码实现的细节。当前,服务端正在 accept() 中等待客户端的 SYN 请求,于是,客户端向服务端发起了 SYN 请求:

// 客户端
int remote_fd;
struct sockaddr_in server_addr;remote_fd = socket(AF_INET, SOCK_STREAM, 0);memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 服务端端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.188"); // 假设服务端 IP 为 192.168.1.188
connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))); // 连接服务端
sys_connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) // net/socket.cstruct socket *sock;sock = sockfd_lookup_light(fd, &err, &fput_needed);.../* @uservaddr: 客户端想连接的目标地址 */err = move_addr_to_kernel(uservaddr, addrlen, &address);...err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); // inet_stream_connect()inet_stream_connect() // net/ipv4/af_inet.c__inet_stream_connect(sock, uaddr, addr_len, flags, 0);switch (sock->state) {...// 发送 SYN 请求包到服务端case SS_UNCONNECTED:...err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */sock->state = SS_CONNECTING; /* 套接字标记为 正在连接状态 */...break;}tcp_v4_connect() // net/ipv4/tcp_ipv4.c// 一些路由等相关的其它处理...tcp_set_state(sk, TCP_SYN_SENT); /* 客户端套接字状态由 CLOSED 转为 SYN-SENT: CLOSED => SYN-SENT */...err = tcp_connect(sk); /* 发送 SYN 包 */struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *buff;.../* 为 SYN 包分配空间 */buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);...tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); /* 构建 SYN 包 */.../* 发送 SYN 包 */err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);...// 接前面的 inet_stream_connect() 流程
inet_stream_connect() // net/ipv4/af_inet.c...switch (sock->state) {// 发送 SYN 请求包到服务端case SS_UNCONNECTED:...err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */sock->state = SS_CONNECTING; /* 套接字标记为 已连接状态 */...break;}...if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {.../** 已向服务端发送 SYN 包,陷入睡眠等待服务端回复 SYN + ACK. * 在收到服务端的 SYN + ACK 后, 内核再回复 ACK 给服务端,* 然后唤醒等待在此处的进程.*/if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))goto out;...}...sock->state = SS_CONNECTED; /* 到此, socket 进入已连接状态 (SS_CONNECTED) */// 等待服务端会送 SYN + ACK ,然后再回复服务端 ACK
inet_wait_for_connect() // net/ipv4/af_inet.cDEFINE_WAIT_FUNC(wait, woken_wake_function);// sk_sleep() //		return &rcu_dereference_raw(sk->sk_wq)->wait;add_wait_queue(sk_sleep(sk), &wait);...while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {...timeo = wait_woken(&wait, TASK_INTERRUPTIBLE, timeo); // 等待服务端回复: SYN + ACK...}...

从上面的分析,我们没有看到客户端(套接字)是怎么接收服务端的 SYN + ACK 的,也没有看到客户端(套接字)在收到服务端的 SYN + ACK 后,回应服务端 ACK 的过程。事实上,就算翻遍整个 connect() 的代码,也找不到这些逻辑,因为处理这个逻辑的是内核网络协议栈的代码。分析这些逻辑,要从网络协议栈接收数据的流程中去找:

// 从网卡数据接收中断入口开始
xxx_nic_interrput()napi_gro_receive()napi_skb_finish()netif_receive_skb_internal()__netif_receive_skb()__netif_receive_skb_core()pt_prev->func() = ip_rcv()ip_rcv_finish()dst_input()ip_local_deliver()ip_local_deliver_finish()ipprot->handler() = tcp_v4_rcv()tcp_v4_rcv() // net/ipv4/tcp_ipv4.cstruct sock *sk;...
lookup:/** 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字。* 在我们的分析上下文中,当前接收的数据报是服务端应客户端 SYN 请求,回复* 的 SYN + ACK 数据包,该数据包发往前面在 inet_wait_for_connect() 中* 等待的客户端套接字 @sk,但 SYN + ACK 会由网路协议栈的 TCP 协议层处理,* 而非客户端套接字本身。*/sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted); ...ret = 0;if (!sock_owned_by_user(sk)) {ret = tcp_v4_do_rcv(sk, skb);}  else if (tcp_add_backlog(sk, skb)) {...}...tcp_v4_do_rcv(sk, skb)...if (tcp_rcv_state_process(sk, skb)) {...}tcp_rcv_state_process(sk, skb)...switch (sk->sk_state) {...case TCP_SYN_SENT: /* 已往服务端发送了 SYN 的客户端处于 TCP_SYN_SENT (connect()  调用) */...queued = tcp_rcv_synsent_state_process(sk, skb, th); /* 收取服务端发送的 SYN + ACK */...return 0;...}tcp_rcv_synsent_state_process(sk, skb, th)...if (th->ack) { // ACK 包标记...if (!th->syn) // SYN + ACKgoto discard_and_undo;...tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);tcp_ack(sk, skb, FLAG_SLOWPATH); // 处理服务端回复包中的 ACK// 包序列号的一些处理tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;...// MTU, MSS 处理tcp_mtup_init(sk);tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);tcp_initialize_rcv_mss(sk);...tcp_finish_connect(sk, skb); // 连接建立完成:套接字 @sk 由 SYN-SENT 转为 ESTABLISHEDtcp_set_state(sk, TCP_ESTABLISHED);...sk_state_store(sk, state); // sk->state = TCP_ESTABLISHED......if (!sock_flag(sk, SOCK_DEAD)) {/* 唤醒等待 server 端 SYN + ACK 的 connect() */sk->sk_state_change(sk) = sock_def_wakeup(sk)struct socket_wq *wq;...wq = rcu_dereference(sk->sk_wq);if (skwq_has_sleeper(wq))wake_up_interruptible_all(&wq->wait); // 唤醒 connect() 调用链中在 inet_wait_for_connect() 中等待连接建立完成的进程...sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);}...if (sk->sk_write_pending ||icsk->icsk_accept_queue.rskq_defer_accept ||icsk->icsk_ack.pingpong) {...} else {tcp_send_ack(sk); /* 客户端 构建 + 发送 ACK 包给服务端 */}}...

到此,客户端的整个建立流程已经完成。接下来我们看从服务端收到客户端的 SYN 开始,然后服务端回复客户端以 SYN + ACK,并最终收到客户端对自身 SYN 回复,建立新的客户端连接套接字的过程:

// 上接前面的 sys_accept4() ,此时客户端为可能的新连接准备好了一个新的套接字,
// 并睡眠等待服务端发送的 SYN 连接请求:
sys_accept4(server_fd, NULL, NULL) // net/socket.cstruct socket *sock, *newsock;sock = sockfd_lookup_light(fd, &err, &fput_needed);/* 为可能连接的客户端准备一个 sock 对象 */newsock = sock_alloc();...// 等待客户端的 SYN 请求err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);inet_accept() // net/ipv4/af_inet.cinet_csk_accept() // net/ipv4/inet_connection_sock.cinet_csk_wait_for_connect()...for (;;) {prepare_to_wait_exclusive(sk_sleep(sk), &wait,TASK_INTERRUPTIBLE);...// 如果 accept 队列(全连接队列)一直为空,则一直等待(只讨论阻塞模式)if (reqsk_queue_empty(&icsk->icsk_accept_queue))timeo = schedule_timeout(timeo);...}...

如同客户端接收来自服务端的 SYN + ACK 回复,以及对服务端的 SYN 回复 ACK 一样,这些在服务端也都是由协议栈完成的,而非 accept() 调用,来看代码实现细节:

// 从网卡数据接收中断入口开始
xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...if (sk->sk_state == TCP_LISTEN) {ret = tcp_v4_do_rcv(sk, skb);goto put_and_return;}...tcp_v4_do_rcv()...if (sk->sk_state == TCP_LISTEN) {/* * 为了防止 SYN flood 攻击,减少连接阶段的资源消耗, 建立了 SYN cookies .* 我们这里假定没有开启 SYN cookies (CONFIG_SYN_COOKIES 配置项关闭)。*/struct sock *nsk = tcp_v4_cookie_check(sk, skb);...} else...if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;...tcp_rcv_state_process()switch (sk->sk_state) {...case TCP_LISTEN:if (th->syn) { /* @skb 为 SYN 报文 */...acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // tcp_v4_conn_request()...if (!acceptable) /* @sk 不接收 @skb, 回发给源头 RESET */return 1;/* @sk 正常接收 SYN @skb */consume_skb(skb);return 0;}...}tcp_v4_conn_request(sk, skb) // net/ipv4/tcp_ipv4.creturn tcp_conn_request(&tcp_request_sock_ops,&tcp_request_sock_ipv4_ops, sk, skb);tcp_conn_request() // net/ipv4/tcp_input.c...struct request_sock *req;...if (sk_acceptq_is_full(sk)) { // 套接字的 @sk accept 队列(全连接队列)已满NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}.../** 为连接请求, 分配轻量级的套接字数据结构 request_sock (指代 server_fd 套接字), * 并设定操作接口 (tcp_request_sock_ops) 。*/req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);struct request_sock *req = reqsk_alloc(ops, sk_listener, attach_listener);req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);if (attach_listener) {...req->rsk_listener = sk_listener; /* 监听套接字 sock: 当前分配的连接请求套接字的完整版 */}req->rsk_ops = ops; /* req->rsk_ops = &tcp_request_sock_ops */req_to_sk(req)->sk_prot = sk_listener->sk_prot;...if (req) {struct inet_request_sock *ireq = inet_rsk(req);...// 标记服务端轻量级代理套接字(struct request_sock)为已接收 SYN-RECEIVED 状态。// TCP_NEW_SYN_RECV 是高内核版本新引入的套接字状态,TCP_SYN_RECV 状态被 TFO 特性使用。ireq->ireq_state = TCP_NEW_SYN_RECV; ...ireq->ireq_family = sk_listener->sk_family;}	tcp_rsk(req)->af_specific = af_ops; /* tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops */...af_ops->init_req(req, sk, skb); /* 设置轻量级套接字的 源、目的 IP: 同 @sk 的 源、目的 IP */tcp_v4_init_req()...if (fastopen_sk) { /* TCP Fast Open(TFO) 特性 */...} else {tcp_rsk(req)->tfo_listener = false;if (!want_cookie)/* 添加连接请求 SYN 到 @sk 的 ehash 表(半连接队列) inet_connection_sock::icsk_accept_queue */inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));reqsk_queue_hash_req(req, timeout); // 将轻量级套接字插入到 ehash 表inet_csk_reqsk_queue_added(sk);reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);atomic_inc(&queue->young);atomic_inc(&queue->qlen);/* 服务端收到 SYN 后,回送 SYN + ACK */af_ops->send_synack(sk, dst, &fl, req, &foc, // tcp_v4_send_synack()!want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);...struct sk_buff *skb;skb = tcp_make_synack(sk, dst, req, foc, synack_type);if (skb) {...err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,ireq->ir_rmt_addr,rcu_dereference(ireq->ireq_opt));...}}reqsk_put(req); /* 释放 Internet 连接请求sock对象(request_sock) */return 0;

到此,服务端回应客户端 SYN 请求以 SYN + ACK 的过程已经完成,从前面对客户端的代码分析,客户端收到服务端的 SYN + ACK 后,会回应服务端的 SYN 请求一个 ACK,看下面的代码细节:

// 从网卡数据接收中断入口开始
xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...process:.../** 已经收到了客户端 SYN 并回应了 SYN + ACK 的服务端轻量级"代理"套接字: * . 服务端轻量级"代理"套接字 @sk / @req 处于 TCP_NEW_SYN_RECV 状态* . 服务端轻量级"代理"套接字 @sk / @req 指代的服务端套接字 req->rsk_listener 处于 TCP_LISTEN 状态*/if (sk->sk_state == TCP_NEW_SYN_RECV) {struct request_sock *req = inet_reqsk(sk); // 服务端轻量级"代理"套接字struct sock *nsk;sk = req->rsk_listener;...if (unlikely(sk->sk_state != TCP_LISTEN)) {inet_csk_reqsk_queue_drop_and_put(sk, req);goto lookup;}...if (!tcp_filter(sk, skb)) { /* 如果数据包没有被 eBPF 过滤掉 */...// 为新的连接建立完整的套接字: 建立 TCP 的套接字数据 (struct sock),// 并将连接数据添加到 accept 队列 (全连接队列)nsk = tcp_check_req(sk, skb, req, false);}...if (nsk == sk) {...} else if (tcp_child_process(sk, nsk, skb)) { // 新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程...}  else {sock_put(sk);return 0;}...}// 看
// 【为新的连接建立完整的套接字: 建立 TCP 的套接字数据 (struct sock),
// 并将连接数据添加到 accept 队列(全连接队列)】
// 的细节:
nsk = tcp_check_req(sk, skb, req, false); // net/ipv4/tcp_minisocks.cchild = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);tcp_v4_syn_recv_sock() // net/ipv4/tcp_ipv4.cstruct sock *newsk;...newsk = tcp_create_openreq_child(sk, req, skb);struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);struct sock *newsk = sk_clone_lock(sk, priority);if (newsk) {...newsk->sk_state = TCP_SYN_RECV;...}........./* 将新连接数据放到 accept 队列(全连接队列) */return inet_csk_complete_hashdance(sk, child, req, own_req);if (own_req) {inet_csk_reqsk_queue_drop(sk, req);// 将建立好连接的套接字从 ehash 表(半连接队列)移除reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);if (inet_csk_reqsk_queue_add(sk, req, child))return child;}...inet_csk_reqsk_queue_add()struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;...if (unlikely(sk->sk_state != TCP_LISTEN)) {...} else {req->sk = child;req->dl_next = NULL;if (queue->rskq_accept_head == NULL)queue->rskq_accept_head = req;elsequeue->rskq_accept_tail->dl_next = req;queue->rskq_accept_tail = req;sk_acceptq_added(sk);sk->sk_ack_backlog++;}...// 继续看
// 【新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程】
// 的细节:
tcp_child_process(sk, nsk, skb)int state = child->sk_state;...if (!sock_owned_by_user(child)) {ret = tcp_rcv_state_process(child, skb);...switch (sk->sk_state) {case TCP_SYN_RECV:...tcp_set_state(sk, TCP_ESTABLISHED); /* 与客户端的通信的套接字装换为已连接状态 ESTABLISHED */...tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);...tcp_initialize_rcv_mss(sk);...break;}.../* Wakeup parent, send SIGIO */if (state == TCP_SYN_RECV && child->sk_state != state)/* 唤醒在 accept() 中等待连接的进程 */parent->sk_data_ready(parent) = sock_def_readable()...wq = rcu_dereference(sk->sk_wq);if (skwq_has_sleeper(wq))wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |POLLRDNORM | POLLRDBAND);sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);...} else {...}...return 0;

到此,3.2 小节中图示的 TCP 连接建立的三次握手过程 已经全部分析完成。当然,这里分析的只是 TCP 连接建立的诸多可能序列当中的一个,其它可能的建立过程,感兴趣的读者可以自行分析源码。另外,对于 SYN Cookies ,TFO(TCP Fast Open),端口重用 等特性,本文也未加讨论。

3.2.1.5 TCP 连接建立过程小结

从上面的分析中应该了解到的是,TCP 连接的建立是一个双向的过程,不管是服务端还是客户端,都会向对端通过 SYN 发起连接请求,而对方也会在收到 SYN 后回应一个 ACK ,这样一个双向连接就建立好了。以这样的方式建立连接的基本理由,是因为 TCP 是全双工通信。从后面的章节也可以看到,拆除连接(四次挥手) 也是一个双向拆除的过程。
另外,连接建立的过程,是由内核协议栈完成的。细心的童鞋会发现,在调用 listen() 后,即使服务端不调用 accept() ,客户端照样可以通过 connect() 建立连接,只是这样的连接,无法正常和服务端进行数据通信。这是因为 listen() 已经建立好了 accept 队列当客户端发起连接时,内核协议栈会把建立好的连接信息放入 accept 队列;而 accept() 只是从该队列中取出建立好的连接信息,它本身并不参与连接的建立过程。用 netstat 观察,可以发现这些 TCP 连接没有进程名信息,取而代之的是 -- 。这样的现象,可能会让初接触 TCP 编程的童鞋大吃一惊。

3.2.2 TCP 连接断开的四次挥手过程分析

当我们的通信完毕,可以通过调用 close() 关闭连接。和连接的建立一样,连接的关闭过程也存在多种可能的序列,本文以 3.2 小节中的连接关闭过程为例来进行分析。

3.2.2.1 客户端通过 close() 向服务端发送 FIN,进入 FIN-WAIT-1 状态
// 客户端发起本端的连接断开请求
close(remote_fd);sys_close() // fs/open.c__close_fd(current->files, fd)filp_close(file, files)// 中间过程有点小复杂,不是这里关注的重点...sock_close()sock_close() // net/socket.c__sock_release(SOCKET_I(inode), inode)if (sock->ops) {...sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.cstruct sock *sk = sock->sk;if (sk) {...sk->sk_prot->close(sk, timeout) = tcp_close()}return 0;...}tcp_close(sk, timeout) // net/ipv4/tcp.c...sk->sk_shutdown = SHUTDOWN_MASK;.../** 关闭连接时,需处理接收缓冲里还没有被应用层读取的数据,* 我们假定关闭连接时,客户端应用已经拿走了接收缓冲里的* 所有数据。* 对于客户端套接字缓冲还有未读取数据的情形,读者可自行分析。*/...sk_mem_reclaim(sk);if (unlikely(tcp_sk(sk)->repair)) {...}  else if (data_was_unread) {...}  else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {...} else if (tcp_close_state(sk)) { // 套接字由 ESTABLISHED 态转为 FIN-WAIT-1tcp_send_fin(sk); // 向服务端发送 FIN 包}...
3.2.2.2 服务端收取客户端 FIN,回以 ACK,进入 CLOSE-WAIT 状态

客户端发送 FIN 包给服务端,服务端收到客户端的 FIN 包后,回应以一个 ACK

xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...process:...if (!sock_owned_by_user(sk)) {ret = tcp_v4_do_rcv(sk, skb);...if (sk->sk_state == TCP_ESTABLISHED) {...tcp_rcv_established(sk, skb, tcp_hdr(skb)); // net/ipv4/tcp_input.c...tcp_send_ack(sk); // 回应 FIN 一个 ACK...tcp_data_queue(sk, skb);if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {...// 服务端收到客户端的 FIN ,除了回一个 ACK 外,// 还要针对 FIN 包做一些和客户端连接的套接字的特定处理:// 状态由 ESTABLISHED 转为 CLOSE-WAIT 等等...if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)tcp_fin(sk);sk->sk_shutdown |= RCV_SHUTDOWN;sock_set_flag(sk, SOCK_DONE);switch (sk->sk_state) {case TCP_SYN_RECV:case TCP_ESTABLISHED:// ESTABLISHED => CLOSE-WAITtcp_set_state(sk, TCP_CLOSE_WAIT);inet_csk(sk)->icsk_ack.pingpong = 1;break;...}......}...return 0;}} else if (...) {...}
3.2.2.3 客户端收取服务端对 FIN 的回应 ACK,进入 FIN-WAIT-2 状态

客户端在调用 close() 向服务端发送 FIN 包后,当前处于 FIN-WAIT-1 状态(如果没有设置SOCK_LINGERclose() 调用也已经返回);服务端对客户端的 FIN 包回应了一个 ACK,自身进入 CLOSE-WAIT 状态;客户端收到这个 ACK 后,进入 FIN-WAIT-2 状态,这时从服务端向客户端发送数据的通道就已经关闭了。看客户端处理 FINACK 包后,进入 FIN-WAIT-2 状态这一过程的代码实现细节:

xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...process:...if (!sock_owned_by_user(sk)) {ret = tcp_v4_do_rcv(sk, skb);...if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;...} else if (tcp_add_backlog(sk, skb)) {...}...tcp_rcv_state_process(sk, skb)...switch (sk->sk_state) {...case TCP_FIN_WAIT1: {...tcp_set_state(sk, TCP_FIN_WAIT2); /* 客户端: FIN-WAIT-1 => FIN-WAIT-2 */sk->sk_shutdown |= SEND_SHUTDOWN;...}...}...
3.2.2.4 服务端通过 close() 向客户端发 FIN,进入 LAST-ACK 状态

此时,TCP 连接已经处于半关闭状态(客户端接收数据通道已经关闭)。同样的,在服务端不再想接收客户端的数据时,调用 close() 向客户端发送 FIN 包,然后服务端套接字由 CLOSE-WAIT 进入 LAST-ACK 状态:

// 关闭服务端和客户端通信的套接字,注意,这里关闭的不是 server_fd
close(client_fd);sys_close() // fs/open.c...sock_close() // net/socket.c__sock_release(SOCKET_I(inode), inode)if (sock->ops) {...sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.cstruct sock *sk = sock->sk;if (sk) {...sk->sk_prot->close(sk, timeout) = tcp_close()}return 0;...}tcp_close(sk, timeout) // net/ipv4/tcp.c...sk->sk_shutdown = SHUTDOWN_MASK;...sk_mem_reclaim(sk);...if (unlikely(tcp_sk(sk)->repair)) {...}  else if (data_was_unread) {...}  else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {...} else if (tcp_close_state(sk)) { // 套接字由 CLOSE-WAIT 态转为 LAST-ACKtcp_send_fin(sk); // 向客户端发送 FIN 包}...
3.2.2.5 客户端收取服务端 FIN,回以 ACK,进入 TIME-WAIT,超时后进入 CLOSED 终态

客户端当前处于 FIN-WAIT-2 状态,收到服务端的 FIN 包后,回复服务端一个 ACK,然后自身进入 TIME-WAIT 状态:

xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...process:...if (!sock_owned_by_user(sk)) {ret = tcp_v4_do_rcv(sk, skb);...if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;...} else if (tcp_add_backlog(sk, skb)) {...}...tcp_rcv_state_process(sk, skb).../* step 7: process the segment text */switch (sk->sk_state) {...case TCP_FIN_WAIT1:case TCP_FIN_WAIT2: // 客户端当前处于 FIN-WAIT-2 状态.../* Fall through */case TCP_ESTABLISHED:tcp_data_queue(sk, skb); // 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态queued = 1;break;}...// 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态
tcp_data_queue(sk, skb)...if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {...if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)tcp_fin(sk);switch (sk->sk_state) {...case TCP_FIN_WAIT2:/* Received a FIN -- send ACK and enter TIME_WAIT. */tcp_send_ack(sk); // 回复给服务端 ACKtcp_time_wait(sk, TCP_TIME_WAIT, 0); // 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAITbreak;...}...}...// 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAIT
tcp_time_wait(sk, TCP_TIME_WAIT, 0);...struct inet_timewait_sock *tw;.../* 分配 TIME-WAIT 套接字数据,初始化包括 TIME-WAIT 超时定时器 等 */tw = inet_twsk_alloc(sk, tcp_death_row, state);...tw = kmem_cache_alloc(sk->sk_prot_creator->twsk_prot->twsk_slab, GFP_ATOMIC);...if (tw) {...tw->tw_state	    = TCP_TIME_WAIT;tw->tw_substate	    = state; // tw->tw_substate = TCP_TIME_WAIT...// 初始化 TIME-WAIT 超时定时器setup_pinned_timer(&tw->tw_timer, tw_timer_handler, (unsigned long)tw);...}if (tw) {...// TIME-WATI 超时时间设置tw->tw_timeout = TCP_TIMEWAIT_LEN;if (state == TCP_TIME_WAIT)timeo = TCP_TIMEWAIT_LEN;...inet_twsk_schedule(tw, timeo); /* 启动 TIME-WAIT 超时定时器 */__inet_twsk_schedule(tw, timeo, false);tw->tw_kill = timeo <= 4*HZ;if (!rearm) {BUG_ON(mod_timer(&tw->tw_timer, jiffies + timeo)); // 启动 TIME-WAIT 超时定时器atomic_inc(&tw->tw_dr->tw_count);} else {...}...}...tcp_done(sk);...tcp_set_state(sk, TCP_CLOSE);...// TIME-WAIT 定时器超时,触发 tw_timer_handler(),
// 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
tw_timer_handler()struct inet_timewait_sock *tw = (struct inet_timewait_sock *)data;...inet_twsk_kill(tw); // 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
3.2.2.6 服务端收取客户端对 FIN 的回应 ACK,进入 CLOSED 终态

上面分析中,客户端收到服务端的 FIN 后,回复服务端一个 ACK,然后自身进入由 FIN-WAIT-2 进入到 TIME-WAIT ,并在超时时间到达后,进入终态 CLOSED 。服务端收到客户端对 FINACK 后,也由 LAST-ACK 转入终态 CLOSED ,到此,整个连接的双向关闭过程终结了。下面看服务端收到客户端对 FINACK,由 LAST-ACK 转入终态 CLOSED 的代码细节:

xxx_nic_interrput()...tcp_v4_rcv()...lookup:/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);...process:...if (!sock_owned_by_user(sk)) {ret = tcp_v4_do_rcv(sk, skb);...if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;...} else if (tcp_add_backlog(sk, skb)) {...}...// 服务端: LAST-ACK => CLOSED
tcp_rcv_state_process(sk, skb)...switch (sk->sk_state) {...case TCP_LAST_ACK:if (tp->snd_una == tp->write_seq) {tcp_update_metrics(sk);tcp_done(sk);tcp_set_state(sk, TCP_CLOSE); // 服务端: LAST-ACK => CLOSEDtcp_clear_xmit_timers(sk);...goto discard;}break;...}...

到此,TCP 连接断开的四次挥手过程已经分析完毕。细心的读者可能会发现,示例代码中的 server_fd 还没有关闭。是的,但本文不打算对此进行分析,相信有了前面的基础,读者自行分析有不会是多困难的事情。

3.2.3 三握四挥小结

3.2.13.2.2 两个小节中的分析,采用了不同排列方式:3.2.1 是先分析了服务端的所有阶段,然后再分析了客户端的所有阶段;3.2.2 是按事件发生的先后顺序,交叉的分析服务端和客户端。这一点读者阅读的时候需要引起注意。这是笔者组织时没有统一规划好造成的,写完后又不想再改了。

4. 抓包三握四挥过程示例

可通过工具 tcpdump 观察 TCP 三握四挥 的过程,请参考博文 Linux: tcpdump抓包示例 。
当然,Wireshark 可能是更好的选择。

5. 参考资料

rfc793: https://www.rfc-editor.org/rfc/rfc793.html
https://mp.weixin.qq.com/s/tCXH8BTrgYaVmwVx_Ek1qA

相关文章:

Linux:TCP三握四挥简析

文章目录 1. 前言2. 背景3. TCP连接的建立和断开3.1 TCP协议状态机3.2 TCP的三握四挥3.2.1 TCP 连接建立的三次握手过程分析3.2.1.1 服务端和客户端套接字的创建3.2.1.2 服务端进入 LISTEN 状态3.2.1.3 服务端在 LISTEN 状态等待客户端的 SYN 请求3.2.1.4 客户端向服务端发送 S…...

2023年全球市场数字干膜测量仪总体规模、主要生产商、主要地区、产品和应用细分研究报告

内容摘要 按收入计&#xff0c;2022年全球数字干膜测量仪收入大约149.2百万美元&#xff0c;预计2029年达到191.6百万美元&#xff0c;2023至2029期间&#xff0c;年复合增长率CAGR为 3.6%。同时2022年全球数字干膜测量仪销量大约 &#xff0c;预计2029年将达到 。2022年中国市…...

Python爬虫脚本的基本组成

一个基本的Python爬虫脚本通常由以下几部分组成&#xff1a; 导入必要的库&#xff1a;Python中有许多库可用于爬虫&#xff0c;如requests用于发送HTTP请求&#xff0c;BeautifulSoup用于解析HTML或XML&#xff0c;selenium用于模拟浏览器操作等。你需要根据你的需求导入相应…...

IIS部署Flask

启用 CGI 安装wfastcgi pip install wfastcgi 启用 wfastcgi 首先以管理员身份运行wfastcgi-enable来在IIS上启用wfastcgi&#xff0c;这个命令位于c:\python_dir\scripts&#xff0c;也就是你需要确保此目录在系统的PATH里&#xff0c;或者你需要cd到这个目录后再执行。 #…...

告警繁杂迷人眼,多源分析见月明

随着数字化浪潮的蓬勃兴起&#xff0c;网络安全问题日趋凸显&#xff0c;面对指数级增长的威胁和告警&#xff0c;传统的安全防御往往力不从心。网内业务逻辑不规范、安全设备技术不成熟都会导致安全设备触发告警。如何在海量众多安全告警中识别出真正的网络安全攻击事件成为安…...

【Python】概述

【Python】概述 特点 Python 是一种面向对象、解释性、弱类型&#xff08;动态数据类型&#xff09;的脚本语言&#xff08;高级程序设计语言&#xff09;。 由于Python是解释型语言&#xff0c;所以具有跨平台特性。 解释型语言&#xff1a; 这意味着开发过程中没有了编译…...

MySQL运维之日志管理

目录 一、日志 1.1错误日志 1.2二进制日志 1.2.1格式 1.2.2查看 1.2.3删除 1.3查询日志...

Yolov5 ONNX导出报错: export failure: Unsupported ONNX opset version: 17

目录 1.问题描述 1.1 报错1 &#xff1a; 1.2 报错 2 2.解决方案 介绍 ONNX&#xff08;Open Neural Network Exchange&#xff09;是一个用于机器学习模型的开放式标准&#xff0c;它旨在使不同的深度学习框架能够将训练好的模型在不同平台上无缝运行。它是由Microsoft和F…...

2023年全球市场儿科PICC导管总体规模、主要生产商、主要地区、产品和应用细分研究报告

内容摘要 按收入计&#xff0c;2022年全球儿科PICC导管收入大约 百万美元&#xff0c;预计2029年达到 百万美元&#xff0c;2023至2029期间&#xff0c;年复合增长率CAGR为 %。同时2022年全球儿科PICC导管销量大约 &#xff0c;预计2029年将达到 。2022年中国市场规模大约为 百…...

Adler-32算法使用Neon优化

1、简单实现 下面代码是Adler-32算法的简单实现,我们来整理一下这段代码的逻辑: A = 1 + D1 + D2 + ... + Dn (mod 65521)B = (1 + D1) + (1 + D1 + D2) + ... + (1 + D1 + D2 + ... + Dn) (mod 65521)= nxD1 + (n-1) x D2 + (n-2) x D3 + ... + Dn + n (mod 65521)Adler-3…...

数据结构-----平衡二叉树

目录 前言 1.平衡二叉树 1.1概念与特点 1.2与二叉排序树比较 1.3判断平衡二叉树 2.平衡二叉树的构建 2.1平衡因子 BF 2.2 LL型失衡&#xff08;右旋&#xff09; 2.3 RR型失衡&#xff08;左旋&#xff09; 2.4 LR型失衡&#xff08;先左旋再右旋&#xff09; 2.5 RL…...

vue3 keepalive翻页保存页面状态

描述 实现页面 A-> B &#xff0c; B->A&#xff08;A保存之前页面状态&#xff0c;不刷新页面&#xff09; // router/index.tsimport { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vueconst router createRouter({h…...

测试工程师思维学习

一、测试工程师应具备什么思维&#xff1f; 透过现象看本质&#xff0c;拒绝“一叶障目” 01、质疑和系统思维 02、创新思维 03、全局思维 04、风险驱动和组合思维 05、用户为中心和比较思维 06、BT思维和架构扩展性思维 二、测试工程师应避免的思维 01、同化现象 02、定位效…...

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— Web APIs(六)

思维导图 一、正则表达式 1.1正则表达式介绍 1.2 语法 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewpor…...

云硬盘和物理硬盘的区别

服务器的硬盘是服务器用来存储数据&#xff0c;一般有云硬盘和物理硬盘两种。云硬盘是云计算平台的虚拟技术的存储服务&#xff0c;将数据存储于云端通过分布式存储架构的形式。物理硬盘是将数据存储在服务器或者是PC端上&#xff0c;存储空间比较大&#xff0c;读写速度也很快…...

数据分析--观察数据处理异常值

引包&#xff1a; import pandas as pd import numpy as np 读取文件&#xff1a; dfpd.read_csv(./HR.csv) 文件见绑定资源&#xff08;来自kaggle的HR.csv&#xff09; 处理过程&#xff1a; 一、从df中拿出处理对象 二、找出缺失值的位置并删除 s1_sdf[satisfactio…...

vue3+elementPlus el-input的type=“number“时去除右边的上下箭头

改成 代码如下 <script lang"ts" setup> import {ref} from vue const inputBtn ref() </script> <template><el-input type"number" v-model"inputBtn" style"width: 80px;" class"no_number">…...

华为云云耀云服务器L实例评测|Elasticsearch的可视化Kibana工具安装 IK分词器的安装和使用

前言 最近华为云云耀云服务器L实例上新&#xff0c;也搞了一台来玩&#xff0c;期间遇到各种问题&#xff0c;在解决问题的过程中学到不少和运维相关的知识。 本篇博客介绍Elasticsearch的可视化Kibana工具安装&#xff0c;以及IK分词器的安装和使用。 其他相关的Elasticsea…...

加密货币交易技巧——人和(一)

交易原则 ​ 本篇主要讲述加密货币交易人需要注意的几个原则。 1.不能贪心&#xff0c;具体表现在做好仓位管理。第一&#xff0c;不要重仓进去&#xff0c;一定要轻仓。第二&#xff0c;开仓就想好本次要赚多少钱&#xff0c;不要太贪&#xff0c;到了预期点就止盈。第三&am…...

数学建模:最优化问题及其求解概述

数学建模&#xff1a;最优化问题及其求解概述 最优化问题定义分类离散优化问题连续优化问题 求解 此博客围绕运筹学以及最优化理论的相关知识&#xff0c;通俗易懂地介绍了最优化问题的定义、分类以及求解算法。 最优化问题 定义 数学优化&#xff08;Mathematical Optimiza…...

企业办理CS资质,怎么选择办理等级?

信息系统建设和服务能力等级证书&#xff08;Information system construction and service—Capability assessment system&#xff0c;简称&#xff1a;CS&#xff09;&#xff0c;由中国电子信息行业联合会组织开展的第三方评估活动&#xff0c;是根据《信息系统建设和服务能…...

华为云云耀云服务器L实例评测|Huawei Cloud EulerOS 自动化环境部署

[toc] Huawei Cloud EulerOS 自动化环境部署 云耀云服务器L实例【Huawei Cloud EulerOS 2.0 64bit】 Python Git Google Chrome Chromedriver Selenium More… 1. Python 镜像创建后自带。 2.Git 拉取项目。 sudo yum install git3. Google Chrome 使用root权限或sudo权…...

从一张表格开始做挖机报价系统

一、前言 历时4个月的挖机销售报价系统进入收尾阶段&#xff0c;由我直接负责与业务方对接&#xff0c;这中间各种折腾真是一言难尽&#xff0c;项目开发过程中还要维护POS系统以及牛奶配送系统&#xff0c;本项目我们采用的是迭代开发&#xff0c;今天讲一下具体的开发过程以…...

Qt扫盲-QTreeView 理论总结

QTreeView 理论使用总结 一、概述二、快捷键绑定三、提高性能四、简单实例1. 设计与概念2. TreeItem类定义3. TreeItem类的实现4. TreeModel类定义5. TreeModel类实现6. 在模型中设置数据 一、概述 QTreeView实现了 model 中item的树形表示。这个类用于提供标准的层次列表&…...

BF算法详解(JAVA语言实现)

目录 BF算法的介绍 图解 JAVA语言实现 BF算法的时间复杂度 BF算法的介绍 BF算法&#xff0c;即暴力(Brute Force)算法&#xff0c;是普通的模式匹配算法&#xff0c;BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配&#xff0c;若相等&#xff0c;则继…...

零基础转行网络工程师,过来人给的一些建议

最近收到好多同学的一些提问&#xff0c;零基础没经验&#xff0c;能不能转行到网络工程师&#xff1f;薪资能有多少&#xff1f;发展前景怎么样&#xff1f; 应该有不少朋友都有这个疑问&#xff0c;那么&#xff0c;今天我尽量给大家做出一个详细的解答&#xff0c;希望能有…...

Vue中如何进行分布式搜索与全文搜索(如Elasticsearch)

在Vue中实现分布式搜索与全文搜索&#xff08;使用Elasticsearch&#xff09; 分布式搜索和全文搜索在现代应用程序中变得越来越重要&#xff0c;因为它们可以帮助用户快速查找和检索大量数据。Elasticsearch是一种强大的分布式搜索引擎&#xff0c;它可以用于实现高性能的全文…...

数据结构-图-最小生成树问题

最小生成树 并查集定义举例说明查找某个元素属于哪个集合代码实现路径压缩 Kruskal算法原理代码实现 Prim算法原理代码实现 并查集 定义 &#x1f680;在一些应用问题中&#xff0c;需要将n个不同的元素分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&…...

使用vite+npm封装组件库并发布到npm仓库

组件库背景&#xff1a;使用elementplusvue封装了一个通过表单组件。通过JSX对el-form下的el-input和el-button等表单进行统一封装&#xff0c;最后达到&#xff0c;通过数据即可一键生成页面表单的功能。 1.使用vite创建vue项目 npm create vitelatest elementplus-auto-form…...

85.最大矩形

单调栈&#xff0c;时间复杂度o(mn)&#xff0c;空间复杂度o(mn) class Solution { public:int maximalRectangle(vector<vector<char>>& matrix) {int mmatrix.size();if(m0){return 0;}int nmatrix[0].size();//记录矩阵中每个元素左边连续1的数量vector<…...

牌具做网站可以吗/营销对企业的重要性

最新2023 win11wsl2 自己编译自己的JDK 参考文章&#xff0c;深入理解JAVA虚拟机 周志明的&#xff0c;结合网上多个实践文章终于完成。 1、安装ubuntu系统 参考我的上一篇文 win11安装wsl2的ubuntu 很简单的配置&#xff0c;系统是默认的ubuntu22.04 记得换个源&#xff0…...

天锐绿盾如何做网站限制/dw网页制作教程

1&#xff0c;其实有时候一直在找借口不去思考这个问题&#xff0c;总是以赶项目为由&#xff0c;没有很认真的思考这个问题&#xff0c;为什么我们要在项目中使用MVP模式&#xff0c;自己也用MVP也已经做了两个项目&#xff0c;而且在网上也看了不少的文章&#xff0c;但是感觉…...

网站css模板/企业推广是什么意思

在输入法的属性里选择紫光的属性&#xff0c;紫光得是4.0或5.0的&#xff0c;在紫光的属性里&#xff0c;在输入法设置里&#xff08;最左边&#xff09;把[输入风格]选输入完拼音&#xff0c;按空格显示&#xff1b;在把[其它]里第一个的对号点掉&#xff0c;把个数该成9个&am…...

营销型网站和展示型网站的区别/app注册接单平台

1、生成器就是对象2、每次调用next()方法时就返回一个值&#xff0c;直到抛出StopIteration异常3、如何创建生成器&#xff1f;很简单&#xff0c;只需写一个普通的函数并包含yield语句&#xff0c;而不是return语句&#xff0c;因此&#xff0c;python会自动将这个函数标记为生…...

如何做网页游戏网站/网络黄页平台网址有哪些

整理这个文档的初衷是自己开始学习的时候没有找到好的教程和文本资料&#xff0c;自己整理一份这样的资料希望能对小伙伴有帮助什么是爬虫&#xff1f;网络爬虫(又被称为网页蜘蛛&#xff0c;网络机器人&#xff0c;在FOAF社区中间&#xff0c;更经常的称为网页追逐者)&#xf…...

佛山专业网站制作/2021关键词搜索排行

dialog使用的十分广泛&#xff0c;今天就介绍下包含了listview的dialog简单应用&#xff0c;其实和普通的dialog一样。1、先布局首先主布局android:id"id/btn_dialog_2"android:layout_width"match_parent"android:layout_height"wrap_content"a…...