深入剖析TCP协议(内容二):从OSI与TCP/IP网络模型到三次握手、四次挥手、状态管理、性能优化及Linux内核源码实现的全面技术指南
文章目录
- 常见问题
- TCP和UDP
- ISN
- UDP
- TCP数据可靠性
- TCP协议如何提高传输效率
- TCP如何处理拥塞
- Socket
- TCP源码
- tcp_v4_connect()
- sys_accept()
- tcp_accept()
- 三次握手
- 客户端发送SYN段
- 服务端发送SYN和ACK处理
- 客户端回复确认ACK段
- 服务端收到ACK段
常见问题
TCP和UDP
TCP和UDP的区别?
-
连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接
- UDP 是不需要连接,即刻传输数据
-
服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点
- UDP 支持一对一、一对多、多对多的交互通信
-
可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达
- UDP 是尽最大努力交付,不保证可靠交付数据
-
拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率
-
首部开销
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
-
传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序
-
分片不同
- TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应该小于 MTU
ISN
① 为什么客户端和服务端的初始序列号 ISN 是不相同的?
如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。
② 初始序列号 ISN 是如何随机产生的?
起始 ISN
是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
ISN = M + F (localhost, localport, remotehost, remoteport)
M
是一个计时器,这个计时器每隔 4 毫秒加 1F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择
UDP
总结
- TCP 向上层提供面向连接的可靠服务 ,UDP 向上层提供无连接不可靠服务
- UDP 没有 TCP 传输可靠,但是可以在实时性要求搞的地方有所作为
- 对数据准确性要求高,速度可以相对较慢的,可以选用TCP
TCP数据可靠性
一句话:通过校验和
、序列号
、确认应答
、超时重传
、连接管理
、流量控制
、拥塞控制
等机制来保证可靠性。
(1)校验和
在数据传输过程中,将发送的数据段都当做一个16位的整数,将这些整数加起来,并且前面的进位不能丢弃,补在最后,然后取反,得到校验和。
发送方:在发送数据之前计算校验和,并进行校验和的填充。接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方进行比较。
(2)序列号
TCP 传输时将每个字节的数据都进行了编号,这就是序列号。序列号的作用不仅仅是应答作用,有了序列号能够将接收到的数据根据序列号进行排序,并且去掉重复的数据。
(3)确认应答
TCP 传输过程中,每次接收方接收到数据后,都会对传输方进行确认应答,也就是发送 ACK 报文,这个 ACK 报文中带有对应的确认序列号,告诉发送方,接收了哪些数据,下一次数据从哪里传。
(4)超时重传
在进行 TCP 传输时,由于存在确认应答与序列号机制,也就是说发送方发送一部分数据后,都会等待接收方发送的 ACK 报文,并解析 ACK 报文,判断数据是否传输成功。如果发送方发送完数据后,迟迟都没有接收到接收方传来的 ACK 报文,那么就对刚刚发送的数据进行重发。
(5)连接管理
就是指三次握手、四次挥手的过程。
(6)流量控制
如果发送方的发送速度太快,会导致接收方的接收缓冲区填充满了,这时候继续传输数据,就会造成大量丢包,进而引起丢包重传等等一系列问题。TCP 支持根据接收端的处理能力来决定发送端的发送速度,这就是流量控制机制。
具体实现方式:接收端将自己的接收缓冲区大小放入 TCP 首部的『窗口大小』字段中,通过 ACK 通知发送端。
(7)拥塞控制
TCP 传输过程中一开始就发送大量数据,如果当时网络非常拥堵,可能会造成拥堵加剧。所以 TCP 引入了慢启动机制
,在开始发送数据的时候,先发少量的数据探探路。
TCP协议如何提高传输效率
一句话:TCP 协议提高效率的方式有滑动窗口
、快重传
、延迟应答
、捎带应答
等。
(1)滑动窗口
如果每一个发送的数据段,都要收到 ACK 应答之后再发送下一个数据段,这样的话我们效率很低,大部分时间都用在了等待 ACK 应答上了。
为了提高效率我们可以一次发送多条数据,这样就能使等待时间大大减少,从而提高性能。窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。
(2)快重传
快重传
也叫高速重发控制
。
那么如果出现了丢包,需要进行重传。一般分为两种情况:
情况一:数据包已经抵达,ACK被丢了。这种情况下,部分ACK丢了并不影响,因为可以通过后续的ACK进行确认;
情况二:数据包直接丢了。发送端会连续收到多个相同的 ACK 确认,发送端立即将对应丢失的数据重传。
(3)延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口大小可能比较小。
- 假设接收端缓冲区为1M,一次收到了512K的数据;如果立刻应答,返回的窗口就是512K;
- 但实际上可能处理端处理速度很快,10ms之内就把512K的数据从缓存区消费掉了;
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
- 如果接收端稍微等一会在应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M;
窗口越大,网络吞吐量就越大,传输效率就越高;我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
(4)捎带应答
在延迟应答的基础上,很多情况下,客户端服务器在应用层也是一发一收的。这时候常常采用捎带应答的方式来提高效率,而ACK响应常常伴随着数据报文共同传输。如:三次握手。
TCP如何处理拥塞
网络拥塞现象是指到达通信网络中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。
拥塞控制的四个阶段:
- 慢启动
- 拥塞避免
- 快速重传
- 快速恢复
Socket
基于TCP协议的客户端和服务器工作:
- 服务端和客户端初始化
socket
,得到文件描述符 - 服务端调用
bind
,将绑定在 IP 地址和端口 - 服务端调用
listen
,进行监听 - 服务端调用
accept
,等待客户端连接 - 客户端调用
connect
,向服务器端的地址和端口发起连接请求 - 服务端
accept
返回用于传输的socket
的文件描述符 - 客户端调用
write
写入数据;服务端调用read
读取数据 - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭
listen 时候参数 backlog 的意义?
Linux内核中会维护两个队列:
- 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
- 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
SYN 队列 与 Accpet 队列
int listen (int socketfd, int backlog)
- 参数一 socketfd 为 socketfd 文件描述符
- 参数二 backlog,这参数在历史内环版本有一定的变化
在早期Linux内核backlog是SYN队列大小,也就是未完成的队列大小。在Linux内核2.2之后,backlog变成accept队列,也就是已完成连接建立的队列长度,所以现在通常认为backlog是accept队列。但是上限值是内核参数somaxconn的大小,也就说accpet队列长度=min(backlog, somaxconn)。
accept 发送在三次握手的哪一步?
我们先看看客户端连接服务端时,发送了什么?
- 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYNC_SENT 状态
- 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYNC_RCVD 状态
- 客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1- 应答包到达服务器端后,服务器端协议栈使得
accept
阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
客户端调用 close 了,连接是断开的流程是什么?
我们看看客户端主动调用了
close
,会发生什么?
- 客户端调用
close
,表明客户端没有数据需要发送了,则此时会向服务端发送FIN报文,进入FIN_WAIT_1状态- 服务端接收到了 FIN 报文,TCP协议栈会为 FIN 包插入一个文件结束符
EOF
到接收缓冲区中,应用程序可以通过read
调用来感知这个 FIN 包。这个EOF
会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为EOF表示在该连接上再无额外数据到达。此时服务端进入 CLOSE_WAIT 状态- 接着,当处理完数据后,自然就会读到
EOF
,于是也调用close
关闭它的套接字,这会使得会发出一个 FIN 包,之后处于 LAST_ACK 状态- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态
- 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态
- 客户端进过
2MSL
时间之后,也进入 CLOSE 状态
TCP源码
tcp_v4_connect()
-
描述: 建立与服务器连接,发送SYN段
-
返回值: 0或错误码
-
代码关键路径:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {..... /* 设置目的地址和目标端口 */inet->dport = usin->sin_port;inet->daddr = daddr;.... /* 初始化MSS上限 */tp->rx_opt.mss_clamp = 536;/* Socket identity is still unknown (sport may be zero).* However we set state to SYN-SENT and not releasing socket* lock select source port, enter ourselves into the hash tables and* complete initialization after this.*/tcp_set_state(sk, TCP_SYN_SENT);/* 设置状态 */err = tcp_v4_hash_connect(sk);/* 将传输控制添加到ehash散列表中,并动态分配端口 */if (err)goto failure;....if (!tp->write_seq)/* 还未计算初始序号 *//* 根据双方地址、端口计算初始序号 */tp->write_seq = secure_tcp_sequence_number(inet->saddr,inet->daddr,inet->sport,usin->sin_port);/* 根据初始序号和当前时间,随机算一个初始id */inet->id = tp->write_seq ^ jiffies;/* 发送SYN段 */err = tcp_connect(sk);rt = NULL;if (err)goto failure;return 0; }
sys_accept()
-
描述: 调用tcp_accept(), 并把它返回的newsk进行连接描述符分配后返回给用户空间。
-
返回值: 连接描述符
-
代码关键路径:
asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen) {struct socket *sock, *newsock;..... sock = sockfd_lookup(fd, &err);/* 获得侦听端口的socket */..... if (!(newsock = sock_alloc()))/* 分配一个新的套接口,用来处理与客户端的连接 */ ..... /* 调用传输层的accept,对TCP来说,是inet_accept */err = sock->ops->accept(sock, newsock, sock->file->f_flags);.... if (upeer_sockaddr) {/* 调用者需要获取对方套接口地址和端口 *//* 调用传输层回调获得对方的地址和端口 */if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) {}/* 成功后复制到用户态 */err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);}..... if ((err = sock_map_fd(newsock)) < 0)/* 为新连接分配文件描述符 */return err; }
tcp_accept()
[注]: 在内核2.6.32以后对应函数为inet_csk_accept().
-
描述: 通过在规定时间内,判断tcp_sock->accept_queue队列非空,代表有新的连接进入.
-
返回值: (struct sock *)newsk;
-
代码关键路径:
struct sock *tcp_accept(struct sock *sk, int flags, int *err) {..../* Find already established connection */if (!tp->accept_queue) {/* accept队列为空,说明还没有收到新连接 */long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);/* 如果套口是非阻塞的,或者在一定时间内没有新连接,则返回 */if (!timeo)/* 超时时间到,没有新连接,退出 */goto out;/* 运行到这里,说明有新连接到来,则等待新的传输控制块 */error = wait_for_connect(sk, timeo);if (error)goto out;}req = tp->accept_queue;if ((tp->accept_queue = req->dl_next) == NULL)tp->accept_queue_tail = NULL;newsk = req->sk;sk_acceptq_removed(sk);tcp_openreq_fastfree(req);....return newsk; }
三次握手
客户端发送SYN段
-
由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()发送,并置为TCP_SYN_SENT.
-
代码关键路径:
/* 构造并发送SYN段 */ int tcp_connect(struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *buff;tcp_connect_init(sk);/* 初始化传输控制块中与连接相关的成员 *//* 为SYN段分配报文并进行初始化 */buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation);if (unlikely(buff == NULL))return -ENOBUFS;/* Reserve space for headers. */skb_reserve(buff, MAX_TCP_HEADER);TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN;TCP_ECN_send_syn(sk, tp, buff);TCP_SKB_CB(buff)->sacked = 0;skb_shinfo(buff)->tso_segs = 1;skb_shinfo(buff)->tso_size = 0;buff->csum = 0;TCP_SKB_CB(buff)->seq = tp->write_seq++;TCP_SKB_CB(buff)->end_seq = tp->write_seq;tp->snd_nxt = tp->write_seq;tp->pushed_seq = tp->write_seq;tcp_ca_init(tp);/* Send it off. */TCP_SKB_CB(buff)->when = tcp_time_stamp;tp->retrans_stamp = TCP_SKB_CB(buff)->when;/* 将报文添加到发送队列上 */__skb_queue_tail(&sk->sk_write_queue, buff);sk_charge_skb(sk, buff);tp->packets_out += tcp_skb_pcount(buff);/* 发送SYN段 */tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));TCP_INC_STATS(TCP_MIB_ACTIVEOPENS);/* Timer for repeating the SYN until an answer. *//* 启动重传定时器 */tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);return 0; }
服务端发送SYN和ACK处理
服务端接收到SYN段后,发送SYN/ACK处理:
-
由tcp_v4_do_rcv()->tcp_rcv_state_process()->tcp_v4_conn_request()->tcp_v4_send_synack().
-
tcp_v4_send_synack()
-
tcp_make_synack(sk, dst, req); ** 根据路由、传输控制块、连接请求块中的构建SYN+ACK段 **
-
ip_build_and_send_pkt(); * 生成IP数据报并发送出去 *
-
-
代码关键路径:
/* 向客户端发送SYN+ACK报文 */ static int tcp_v4_send_synack(struct sock *sk, struct open_request *req,struct dst_entry *dst) {int err = -1;struct sk_buff * skb;/* First, grab a route. *//* 查找到客户端的路由 */if (!dst && (dst = tcp_v4_route_req(sk, req)) == NULL)goto out;/* 根据路由、传输控制块、连接请求块中的构建SYN+ACK段 */skb = tcp_make_synack(sk, dst, req);if (skb) {/* 生成SYN+ACK段成功 */struct tcphdr *th = skb->h.th;/* 生成校验码 */th->check = tcp_v4_check(th, skb->len,req->af.v4_req.loc_addr,req->af.v4_req.rmt_addr,csum_partial((char *)th, skb->len,skb->csum));/* 生成IP数据报并发送出去 */err = ip_build_and_send_pkt(skb, sk, req->af.v4_req.loc_addr,req->af.v4_req.rmt_addr,req->af.v4_req.opt);if (err == NET_XMIT_CN)err = 0;}out:dst_release(dst);return err; }
客户端回复确认ACK段
-
由tcp_v4_do_rcv()->tcp_rcv_state_process().当前客户端处于TCP_SYN_SENT状态。
-
tcp_rcv_synsent_state_process(); * tcp_rcv_synsent_state_process处理SYN_SENT状态下接收到的TCP段 *
-
tcp_ack(); ** 处理接收到的ack报文 **
-
tcp_send_ack(); * 在主动连接时,向服务器端发送ACK完成连接,并更新窗口 *
- alloc_skb(); ** 构造ack段 **
- tcp_transmit_skb(); ** 将ack段发出 **
-
tcp_urg(sk, skb, th); ** 处理完第二次握手后,还需要处理带外数据 **
-
tcp_data_snd_check(sk); * 检测是否有数据需要发送 *
- 检查sk->sk_send_head队列上是否有待发送的数据。
-
tcp_write_xmit(); ** 将TCP发送队列上的段发送出去 **
-
-
代码关键路径:
/* 在SYN_SENT状态下处理接收到的段,但是不处理带外数据 */
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,struct tcphdr *th, unsigned len)
{struct tcp_sock *tp = tcp_sk(sk);int saved_clamp = tp->rx_opt.mss_clamp;/* 解析TCP选项并保存到传输控制块中 */tcp_parse_options(skb, &tp->rx_opt, 0);if (th->ack) {/* 处理ACK标志 *//* rfc793:* "If the state is SYN-SENT then* first check the ACK bit* If the ACK bit is set* If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send* a reset (unless the RST bit is set, if so drop* the segment and return)"** We do not send data with SYN, so that RFC-correct* test reduces to:*/if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt)goto reset_and_undo;if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&!between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,tcp_time_stamp)) {NET_INC_STATS_BH(LINUX_MIB_PAWSACTIVEREJECTED);goto reset_and_undo;}/* Now ACK is acceptable.** "If the RST bit is set* If the ACK was acceptable then signal the user "error:* connection reset", drop the segment, enter CLOSED state,* delete TCB, and return."*/if (th->rst) {/* 收到ACK+RST段,需要tcp_reset设置错误码,并关闭套接口 */tcp_reset(sk);goto discard;}/* rfc793:* "fifth, if neither of the SYN or RST bits is set then* drop the segment and return."** See note below!* --ANK(990513)*/if (!th->syn)/* 在SYN_SENT状态下接收到的段必须存在SYN标志,否则说明接收到的段无效,丢弃该段 */goto discard_and_undo;/* rfc793:* "If the SYN bit is on ...* are acceptable then ...* (our SYN has been ACKed), change the connection* state to ESTABLISHED..."*//* 从首部标志中获取显示拥塞通知的特性 */TCP_ECN_rcv_synack(tp, th);if (tp->ecn_flags&TCP_ECN_OK)/* 如果支持ECN,则设置标志 */sk->sk_no_largesend = 1;/* 设置与窗口相关的成员变量 */tp->snd_wl1 = TCP_SKB_CB(skb)->seq;tcp_ack(sk, skb, FLAG_SLOWPATH);/* Ok.. it's good. Set up sequence numbers and* move to established.*/tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;/* RFC1323: The window in SYN & SYN/ACK segments is* never scaled.*/tp->snd_wnd = ntohs(th->window);tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq, TCP_SKB_CB(skb)->seq);if (!tp->rx_opt.wscale_ok) {tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;tp->window_clamp = min(tp->window_clamp, 65535U);}if (tp->rx_opt.saw_tstamp) {/* 根据是否支持时间戳选项来设置传输控制块的相关字段 */tp->rx_opt.tstamp_ok = 1;tp->tcp_header_len =sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;tcp_store_ts_recent(tp);} else {tp->tcp_header_len = sizeof(struct tcphdr);}/* 初始化PMTU、MSS等成员变量 */if (tp->rx_opt.sack_ok && sysctl_tcp_fack)tp->rx_opt.sack_ok |= 2;tcp_sync_mss(sk, tp->pmtu_cookie);tcp_initialize_rcv_mss(sk);/* Remember, tcp_poll() does not lock socket!* Change state from SYN-SENT only after copied_seq* is initialized. */tp->copied_seq = tp->rcv_nxt;mb();tcp_set_state(sk, TCP_ESTABLISHED);/* Make sure socket is routed, for correct metrics. */tp->af_specific->rebuild_header(sk);tcp_init_metrics(sk);/* Prevent spurious tcp_cwnd_restart() on first data* packet.*/tp->lsndtime = tcp_time_stamp;tcp_init_buffer_space(sk);/* 如果启用了连接保活,则启用连接保活定时器 */if (sock_flag(sk, SOCK_KEEPOPEN))tcp_reset_keepalive_timer(sk, keepalive_time_when(tp));if (!tp->rx_opt.snd_wscale)/* 首部预测 */__tcp_fast_path_on(tp, tp->snd_wnd);elsetp->pred_flags = 0;if (!sock_flag(sk, SOCK_DEAD)) {/* 如果套口不处于SOCK_DEAD状态,则唤醒等待该套接口的进程 */sk->sk_state_change(sk);sk_wake_async(sk, 0, POLL_OUT);}/* 连接建立完成,根据情况进入延时确认模式 */if (sk->sk_write_pending || tp->defer_accept || tp->ack.pingpong) {/* Save one ACK. Data will be ready after* several ticks, if write_pending is set.** It may be deleted, but with this feature tcpdumps* look so _wonderfully_ clever, that I was not able* to stand against the temptation 8) --ANK*/tcp_schedule_ack(tp);tp->ack.lrcvtime = tcp_time_stamp;tp->ack.ato = TCP_ATO_MIN;tcp_incr_quickack(tp);tcp_enter_quickack_mode(tp);tcp_reset_xmit_timer(sk, TCP_TIME_DACK, TCP_DELACK_MAX);discard:__kfree_skb(skb);return 0;} else {/* 不需要延时确认,立即发送ACK段 */tcp_send_ack(sk);}return -1;}/* No ACK in the segment */if (th->rst) {/* 收到RST段,则丢弃传输控制块 *//* rfc793:* "If the RST bit is set** Otherwise (no ACK) drop the segment and return."*/goto discard_and_undo;}/* PAWS check. *//* PAWS检测失效,也丢弃传输控制块 */if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp && tcp_paws_check(&tp->rx_opt, 0))goto discard_and_undo;/* 在SYN_SENT状态下收到了SYN段并且没有ACK,说明是两端同时打开 */if (th->syn) {/* We see SYN without ACK. It is attempt of* simultaneous connect with crossed SYNs.* Particularly, it can be connect to self.*/tcp_set_state(sk, TCP_SYN_RECV);/* 设置状态为TCP_SYN_RECV */if (tp->rx_opt.saw_tstamp) {/* 设置时间戳相关的字段 */tp->rx_opt.tstamp_ok = 1;tcp_store_ts_recent(tp);tp->tcp_header_len =sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;} else {tp->tcp_header_len = sizeof(struct tcphdr);}/* 初始化窗口相关的成员变量 */tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;/* RFC1323: The window in SYN & SYN/ACK segments is* never scaled.*/tp->snd_wnd = ntohs(th->window);tp->snd_wl1 = TCP_SKB_CB(skb)->seq;tp->max_window = tp->snd_wnd;TCP_ECN_rcv_syn(tp, th);/* 从首部标志中获取显式拥塞通知的特性。 */if (tp->ecn_flags&TCP_ECN_OK)sk->sk_no_largesend = 1;/* 初始化MSS相关的成员变量 */tcp_sync_mss(sk, tp->pmtu_cookie);tcp_initialize_rcv_mss(sk);/* 向对端发送SYN+ACK段,并丢弃接收到的SYN段 */tcp_send_synack(sk);
#if 0/* Note, we could accept data and URG from this segment.* There are no obstacles to make this.** However, if we ignore data in ACKless segments sometimes,* we have no reasons to accept it sometimes.* Also, seems the code doing it in step6 of tcp_rcv_state_process* is not flawless. So, discard packet for sanity.* Uncomment this return to process the data.*/return -1;
#elsegoto discard;
#endif}/* "fifth, if neither of the SYN or RST bits is set then* drop the segment and return."*/discard_and_undo:tcp_clear_options(&tp->rx_opt);tp->rx_opt.mss_clamp = saved_clamp;goto discard;reset_and_undo:tcp_clear_options(&tp->rx_opt);tp->rx_opt.mss_clamp = saved_clamp;return 1;
}
服务端收到ACK段
-
由tcp_v4_do_rcv()->tcp_rcv_state_process().当前服务端处于TCP_SYN_RECV状态变为TCP_ESTABLISHED状态。
-
代码关键路径:
/* 除了ESTABLISHED和TIME_WAIT状态外,其他状态下的TCP段处理都由本函数实现 */ int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,struct tcphdr *th, unsigned len) {struct tcp_sock *tp = tcp_sk(sk);int queued = 0;tp->rx_opt.saw_tstamp = 0;switch (sk->sk_state) {...../* SYN_RECV状态的处理 */if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&/* 解析TCP选项,如果首部中存在时间戳选项 */tcp_paws_discard(tp, skb)) {/* PAWS检测失败,则丢弃报文 */if (!th->rst) {/* 如果不是RST段 *//* 发送DACK给对端,说明接收到的TCP段已经处理过 */NET_INC_STATS_BH(LINUX_MIB_PAWSESTABREJECTED);tcp_send_dupack(sk, skb);goto discard;}/* Reset is accepted even if it did not pass PAWS. */}/* step 1: check sequence number */if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {/* TCP段序号无效 */if (!th->rst)/* 如果TCP段无RST标志,则发送DACK给对方 */tcp_send_dupack(sk, skb);goto discard;}/* step 2: check RST bit */if(th->rst) {/* 如果有RST标志,则重置连接 */tcp_reset(sk);goto discard;}/* 如果有必要,则更新时间戳 */tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq);/* step 3: check security and precedence [ignored] *//* step 4:** Check for a SYN in window.*/if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {/* 如果有SYN标志并且序号在接收窗口内 */NET_INC_STATS_BH(LINUX_MIB_TCPABORTONSYN);tcp_reset(sk);/* 复位连接 */return 1;}/* step 5: check the ACK field */if (th->ack) {/* 如果有ACK标志 *//* 检查ACK是否为正常的第三次握手 */int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);switch(sk->sk_state) {case TCP_SYN_RECV:if (acceptable) {tp->copied_seq = tp->rcv_nxt;mb();/* 正常的第三次握手,设置连接状态为TCP_ESTABLISHED */tcp_set_state(sk, TCP_ESTABLISHED);sk->sk_state_change(sk);/* Note, that this wakeup is only for marginal* crossed SYN case. Passively open sockets* are not waked up, because sk->sk_sleep ==* NULL and sk->sk_socket == NULL.*/if (sk->sk_socket) {/* 状态已经正常,唤醒那些等待的线程 */sk_wake_async(sk,0,POLL_OUT);}/* 初始化传输控制块,如果存在时间戳选项,同时平滑RTT为0,则需计算重传超时时间 */tp->snd_una = TCP_SKB_CB(skb)->ack_seq;tp->snd_wnd = ntohs(th->window) <<tp->rx_opt.snd_wscale;tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq,TCP_SKB_CB(skb)->seq);/* tcp_ack considers this ACK as duplicate* and does not calculate rtt.* Fix it at least with timestamps.*/if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&!tp->srtt)tcp_ack_saw_tstamp(tp, 0);if (tp->rx_opt.tstamp_ok)tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;/* Make sure socket is routed, for* correct metrics.*//* 建立路由,初始化拥塞控制模块 */tp->af_specific->rebuild_header(sk);tcp_init_metrics(sk);/* Prevent spurious tcp_cwnd_restart() on* first data packet.*/tp->lsndtime = tcp_time_stamp;/* 更新最近一次发送数据包的时间 */tcp_initialize_rcv_mss(sk);tcp_init_buffer_space(sk);tcp_fast_path_on(tp);/* 计算有关TCP首部预测的标志 */} else {return 1;}break;.....}} elsegoto discard;...../* step 6: check the URG bit */tcp_urg(sk, skb, th);/* 检测带外数据位 *//* tcp_data could move socket to TIME-WAIT */if (sk->sk_state != TCP_CLOSE) {/* 如果tcp_data需要发送数据和ACK则在这里处理 */tcp_data_snd_check(sk);tcp_ack_snd_check(sk);}if (!queued) { /* 如果段没有加入队列,或者前面的流程需要释放报文,则释放它 */ discard:__kfree_skb(skb);}return 0; }
相关文章:
深入剖析TCP协议(内容二):从OSI与TCP/IP网络模型到三次握手、四次挥手、状态管理、性能优化及Linux内核源码实现的全面技术指南
文章目录 常见问题TCP和UDPISNUDPTCP数据可靠性TCP协议如何提高传输效率TCP如何处理拥塞 SocketTCP源码tcp_v4_connect()sys_accept()tcp_accept()三次握手客户端发送SYN段服务端发送SYN和ACK处理客户端回复确认ACK段服务端收到ACK段 常见问题 TCP和UDP TCP和UDP的区别&#…...
流程架构是什么?为什么要构建流程架构,以及如何构建流程结构?
本文从:流程架构是什么?为什么要构建流程架构?如何构建流程结构三个方面来介绍。 一、首先,我们来了解流程架构是什么? 流程架构是人体的骨架,是大楼的砌筑,是课本的目录,是流程管理…...
Visium HD多样本拼片拆分
Visium HD实验的时候一个捕获区域内可以包含多个样本拼片(例如多个组织切片或不同样本的排列)是常见的实验设计,多样本拼片能够提升实验效率,单张玻片处理多个样本,降低试剂和测序成本,后续分析的时候只需要…...
3DMAX零售商店生成插件RetailStore自定义贴图库方法详解
3DMAX零售商店生成插件——RetailStore,是一款兼具简洁性与复杂性的工具,专为通过样条线快速创建零售商店而设计。用户只需绘制一条街道廓线,轻点鼠标,即可生成一排随机的零售商店。该插件会在每个样条线段上自动生成一个店铺&…...
从性能到安全:大型网站系统架构演化的 13 个核心维度
大型网站系统架构的演化是一个复杂的过程,涉及到多个维度的技术内容,从关键维度进行详细分析: 1.性能维度 缓存技术:包括浏览器缓存、CDN(内容分发网络)缓存、服务器端缓存(如 Memcached、Red…...
昆仑万维开源SkyReels-V2,近屿智能紧跟AI技术趋势
昆仑万维 SkyReels 团队正式发布并开源全球首个采用扩散强迫框架的无限时长电影生成模型 SkyReels-V2,其通过融合多模态大语言模型、多阶段预训练、强化学习与扩散强迫框架实现协同优化,推动视频生成技术进入新阶段。该模型聚焦解决现有技术在提示词遵循…...
Milvus(4):创建 Collections
1 创建 Collections 可以通过定义 Schema、索引参数、度量类型以及创建时是否加载来创建一个 Collection。 1.1 集合概述 Collection 是一个二维表,具有固定的列和变化的行。每列代表一个字段,每行代表一个实体。要实现这样的结构化数据管理,…...
数据预处理:前缀和算法详解
数据预处理:前缀和算法详解 文章目录 数据预处理:前缀和算法详解1.算法原理2.算法作用3.C代码实现4.实战题目 1.算法原理 基本概念 前缀和(Prefix Sum)是一种常用的数据预处理技术,它可以快速求解区间和问题…...
盈达科技:登顶GEO优化全球制高点,以AICC定义AI时代内容智能优化新标杆
一、技术制高点——全球独创AICC系统架构,构建AI内容优化新范式 作为全球首个实现AI内容全链路优化的技术供应商,盈达科技凭借AICC智能协同中心(自适应内容改造、智能数据投喂、认知权重博弈、风险动态响应四大引擎)&#…...
【Linux】详细介绍进程的概念
目录 一、初识进程概念 真正的进程概念如下: 二、Linux中PCB的操作系统学科叫法:task_struct 1、简单认识task_ struct内容分类 2、问题:操作系统怎么知道当前程序执行到哪一行代码了? 三、linux关于进程的常用指令ÿ…...
mybatis框架补充
一,#{} 和${}区别 1.传数值 #{} 占位符,是经过预编译的,编译好SQL语句再取值,#方式能够防止sql注入 eg:#{}:delete from admin where id #{id} 结果: dalete from admin where id &#x…...
Alertmanager的安装和详细使用步骤总结
一、安装步骤 1. 二进制安装 下载与解压 从GitHub下载最新版本(如v0.23.0):wget https://github.com/prometheus/alertmanager/releases/download/v0.23.0/alertmanager-0.23.0.linux-amd64.tar.gz tar -xzf alertmanager-0.23.0.linux-amd6…...
C++学习:六个月从基础到就业——C++学习之旅:STL迭代器系统
C学习:六个月从基础到就业——C学习之旅:STL迭代器系统 本文是我C学习之旅系列的第二十四篇技术文章,也是第二阶段"C进阶特性"的第二篇,主要介绍C STL迭代器系统。查看完整系列目录了解更多内容。 引言 在上一篇文章中…...
缓存与数据库一致性方案
一、缓存更新策略概述 在现代分布式系统中,缓存作为数据库的前置层,能显著提升系统性能。然而,缓存与数据库之间的数据一致性是一个经典难题。以下是三种常见的缓存更新策略及其优缺点分析。 二、方案对比分析 方案一:直接更新…...
国内ip地址怎么改?详细教程
在中国,更改IP地址需要遵守规则,并确保所有操作合规。在特定情况下,可能需要修改IP地址以满足不同需求或解决特定问题。以下是一些常见且合法的IP地址变更方法及注意事项: 一、理解IP地址 IP地址是设备在网络中的唯一标识&#x…...
通过Quartus II实现Nios II编程
目录 一、认识Nios II二、使用Quartus II 18.0Lite搭建Nios II硬件部分三、软件部分四、运行项目 一、认识Nios II Nios II软核处理器简介 Nios II是Altera公司推出的一款32位RISC嵌入式处理器,专门设计用于在FPGA上运行。作为软核处理器,Nios II可以通…...
拥抱基因体检,迎接精准健康管理新时代
2025年4月20日,由早筛网、细胞科技网联合中国食品药品企业质量安全促进会细胞医药分会、中国抗衰老促进会健康管理工作委员会、中国抗癌协会肿瘤分子医学专业委员会、广东省保健协会,伯温生物冠名支持的《基因体检赋能精准健康管理新时代》圆满召开。 伯…...
QT容器类控件及其属性
Group Box 使用QGroupBox实现一个带有标题的分组框,可以把其它的控件放到里面作为一组 例: 核心属性 属性 说明 title 分组框的标题 alignment 分组框内部内容的对齐方式 flat 是否”扁平模式” checkable 是否可选中 设为true,则…...
云原生--CNCF-3-核心工具介绍(容器和编排、服务网格和通信、监控和日志、运行时和资源管理,安全和存储、CI/CD等)
1、核心工具分类介绍 (1)、容器编排与管理 1、Docker 它是一款轻量级的容器化技术,可把应用及其依赖打包成独立的容器。借助Docker,开发者能够确保应用在不同环境中保持一致的运行状态。比如在开发环境中创建的容器,…...
网络基础知识
文章目录 一、网络架构1. 网络架构图2. 各层级功能3. 机房网络常见问题及解决方案 二、交换技术1. 交换技术基础2. 交换技术分类3. 广播域相关概念4. ARP 协议5. 三层交换机6. VLAN(虚拟局域网) 三、路由技术1. 路由器端口类型及功能2. 路由器功能3. 路由…...
第3课:运算符与流程控制——JS的“决策者”
一切美好都值得你全力以赴。即使过程艰难,但只要坚持,必有回响。加油! 欢迎来到「JavaScript 魔法学院」第 3 课!今天我们将化身代码世界的“指挥官”,用运算符计算数据,用流程控制做出决策!文…...
VUE3中使用echarts,配置都正确,不出现tooltip
在vue3中使用echarts,出现个别问题,需要注意echars对象的定义,不能使用reactive或者ref定义响应式变量,要使用shallowRef ; shallowRef 是 Vue 3 提供的一个 API,它创建一个响应式引用(ref),但…...
Python实现邮件发送
一、创造灵感 这几天在指导学生做毕业设计,学生做的是跌倒检测。然后我突然想到,如果这个系统只是单纯地从视频流中检测到人的跌倒动作,其实并没有任何的用途和意义。于是,我又想到,如果跌倒的检测结果,能…...
OSPF的优化
OSPF的优化 1、汇总 --- 减少骨干区域LSA的更新量 汇总 --- 区域汇总 域间路由汇总 --- 在ABR设备上,针对3类LSA进行汇总 [r1-ospf-1-area-0.0.0.2]abr-summary 192.168.0.0 255.255.252.0 注:在进行域间路由汇总时,必须是ABR设备自己通过拓…...
IP-guard离线卸载客户端及清除策略说明
1、控制台生成客户端工具 控制台选择“工具-客户端工具-客户端离线辅助工具”,可生成客户端离线卸载工具及 离线策略清除工具,打包生成exe后,可在离线的客户端上运行,实现离线情况下对客户端 进行卸载或者清除策略。 在控制台上“…...
腾讯秋招面试题:bug生命周期中有哪些状态?
在软件测试中,Bug生命周期是质量管控的核心脉络。不同企业的流程或有差异,但核心状态遵循行业通用规范。以下以腾讯TAPD缺陷管理模型为基础,结合互联网大厂主流实践,详解Bug生命周期的关键状态及流转逻辑。 一、基础状态图谱 图表 代码 下载 确认有效 非缺陷/需求不符…...
PyCharm 链接 Podman Desktop 的 podman-machine-default Linux 虚拟环境
#工作记录 PyCharm Community 连接到Podman Desktop 的 podman-machine-default Linux 虚拟环境详细步骤 1. 准备工作 确保我们已在 Windows 系统中正确安装并启动了 Podman Desktop。 我们将通过 Podman Desktop 提供的名为 podman-machine-default 的 Fedora Linux 41 WSL…...
学习海康VisionMaster之卡尺工具
一:进一步学习了 今天学习下VisionMaster中的卡尺工具:主要用于测量物体的宽度、边缘的特征的位置以及图像中边缘对的位置和间距 二:开始学习 1:什么是卡尺工具? 如果我需要检测芯片的每一个PIN的宽度和坐标ÿ…...
私有知识库 Coco AI 实战(二):摄入 MongoDB 数据
在之前的文章中,我们介绍过如何使用《 Logstash 迁移 MongoDB 数据到 Easyseach》,既然 Coco AI 后台数据存储也使用 Easysearch,我们能否直接把 MongoDB 的数据迁移到 Coco AI 的 Easysearch,使用 Coco AI 对数据进行检索呢&…...
【C/C++】插件机制:基于工厂函数的动态插件加载
本文介绍了如何通过 C 的 工厂函数、动态库(.so 文件)和 dlopen / dlsym 实现插件机制。这个机制允许程序在运行时动态加载和调用插件,而无需在编译时知道插件的具体类型。 一、 动态插件机制 在现代 C 中,插件机制广泛应用于需要…...
硬核科普丨2025年安全、高效网络准入控制系统深度解析
阳途网络准入控制系统(Network Access Control,简称NAC)是当代网络安全领域的重要工具,有效防止未经授权的访问和数据泄露,保障网络资源的安全性和完整性。本文将深入探讨阳途网络准入控制系统的的重要性和作用。 一、…...
2025知识管理趋势解析:AI、协作与用户体验引领变革
知识管理软件是多元化的商业工具,包括知识库系统、wiki、协作平台、文档管理工具、聊天机器人和帮助系统,针对企业信息爆炸式增长的趋势,这些工具正逐步成为提升知识工作者效率和客户服务水平的关键支撑。 知识成为新资产:知识工…...
杂谈-有感而发
今天又拜读了线性代数的几何意义这本书,感觉确实是很不错的,从几何角度讲解线性代数,而且将线性代数、几何、数学、物理有机结合起来了,不仅仅是可视化,重要的是能便于自己独立思考下去。 从大一开始,就…...
使用nodeJs的express+axios+cors做代理
使用nodeJs的expressaxioscors做代理 前端在请求后端时通常会遇到跨域cors问题,如果只在本地开发可以通过webpack或vite的proxy设置。但如果需要在线上或者其他地方绕过跨域,可以使用代理的方法。 1. 创建文件夹 并创建以下文件 package.json {"…...
SQL进阶知识:四、索引优化
今天介绍下关于索引优化的详细介绍,并结合MySQL数据库提供实际例子。 索引优化是数据库性能优化的关键环节之一,尤其是在处理大量数据时。索引可以加快查询速度,减少数据扫描范围,但不当的索引设计也可能导致性能问题。以下是关于…...
C++初阶-类和对象(下)
目录 1.再探构造函数 2.类型转换 3.友元 4.static成员 5.内部类 6.匿名对象 *7.对象拷贝时的编译器优化(非必学) 8.总结 1.再探构造函数 (1)之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值&#…...
RPC通信原理实战
RPC概念及RPC请求响应流程 RPC概念引入 假设有两个模块,用户和订单 在单体项目中,两个模块被打包到一个包,都处在一个tomcat进程中,用户模块调用订单模块属于同一进程内的本地调用 在微服务项目中,用户模块和订单模…...
自动创建 中国古代故事人物一致性图画,看看扣子的空间是否能达到你的满意,自媒体的福音?
欢迎来到涛涛聊AI 看效果: AI绘画最难的人物一致性问题,每次得到的结果都不一致。 官方介绍: 扣子空间是你和 AI Agent 协同办公的最佳场所。 在扣子空间里,精通各项技能的「通用实习生」,各行各业的「领域专家」&…...
【Unity笔记】Unity音效管理:ScriptableObject配置 + 音量控制 + 编辑器预览播放自动化实现
摘要: 本文介绍了如何在 Unity 中构建一个高效的音效管理系统,通过 ScriptableObject 实现音效集中配置,支持为每个音效单独设置音量,并通过自定义 Editor 实现音效的可视化预览播放与下拉选择播放功能,整个系统无场景…...
面向高可靠场景的RISC-V低功耗MCU硬件安全设计
该RISC-V架构的低功耗MCU通过多项硬件级安全技术满足工业控制、汽车电子及物联网等领域对可靠性与安全性的需求。其核心特性包含六个方面: 在数据完整性验证与固件安全升级方面,该MCU通过硬件级校验机制(如CRC、哈希算法)确保固件…...
OpenVINO教程(五):实现YOLOv11+OpenVINO实时视频目标检测
目录 实现讲解效果展示完整代码 本文作为上篇博客的延续,在之前实现了图片推理的基础上,进一步介绍如何进行视频推理。 实现讲解 首先,我们需要对之前的 predict_and_show_image 函数进行拆分,将图像显示与推理器(pre…...
【新能源科学与技术】MATALB/Simulink小白教程(一)实验文档【新能源电力转换与控制仿真】
DP读书:新能源科学与工程——专业课「新能源发电系统」 2025a 版本 MATLAB下面进入正题 仿真一:Buck 电路一、仿真目的二、仿真内容(一)Buck电路基本构成及工作原理(二)Buck电路仿真模型及元件连接…...
软件项目实施全流程及交付物清单
需求分析 -> 概要设计 -> 详细设计 -> 开发 -> 测试 -> 部署 -> 上线运行 一、确认项目目标、范围和团队成员 二、收集和分析客户需求,确定需求规格 三、制定详细的项目计划,包括时间表、资源计划、预算 四、系统架构设计…...
Docker安装ES :确保 Kibana 正确连接 Elasticsearch
在使用 Docker 部署 ELK(Elasticsearch、Logstash、Kibana)堆栈时,正确的服务配置和依赖管理至关重要。本文将分享如何优化 Docker Compose 配置,确保 Kibana 能稳定连接到 Elasticsearch,并提供故障排查建议 cd /opt/…...
云贝餐饮 最新 V3 独立连锁版 全开源 多端源码 VUE 可二开
云贝餐饮源码 最新 V3 独立连锁版 全开源 多端源码 VUE 可二开 vue uniapp 独家优化版本 后台管理 Vue 源文件 后台管理文件 yii2 升级包 (全开源) 收银台 Vue 源文件 装修 Vue 源文件 商家端 uni-app 源文件 用户端 uni-app 源文件 数据库文件 云贝餐饮…...
python异步协程async调用过程图解
1.背景: 项目中有用到协程,但是对于协程,线程,进程的区别还不是特别了解,所以用图示的方式画了出来,用于理清三者的概念。 2.概念理解: 2.1协程,线程,进程包含关系 一…...
【Java学习笔记】选择结构
选择结构 内容结构 一、顺序结构 二、分支控制 (1)单分支 (2)双分支 (3)多分支 (4)嵌套分支 (5)switch 分支结构 三、switch和if的比较 一、顺序结构…...
java Nacos
windows下载nacos,版本2.x的,1.0的话会出现“Server check fail, please check server 127.0.0.1 ,port 9848 is available , error {}” 这个鬼问题 下载链接 这里用的是2.0.4版本,启动的话用 startup.cmd -m standalone,单体启…...
c语言之杂识
前言 主要说一下c语言的杂知识 一、预处理指令 1. #include #include指令用于包含其他头文件的内容到当前文件中。这允许您在程序中使用其他模块定义的函数和变量。 示例: #include <stdio.h> // 包含标准输入输出头文件int main() {printf("Hello…...
CSS内边距、外边距、边框
CSS内边距 内边距 :内容区(Content)与边框(Border)之间的透明间距,用于增加元素内部的留白。不影响元素内容本身的尺寸(如文字大小),但会增大元素的总占用空间。 以下是…...