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

TCP可靠传输的秘密:从滑动窗口到拥塞控制

TCP可靠传输的秘密:从滑动窗口到拥塞控制

在这里插入图片描述

目录

  • 一、前言
  • 二、TCP可靠性机制概述
  • 三、ACK确认机制
    • 3.1 序列号和确认号
    • 3.2 累积确认
    • 3.3 选择确认SACK
    • 3.4 捎带确认
  • 四、超时重传机制
    • 4.1 超时重传的基本原理
    • 4.2 RTO计算
    • 4.3 快速重传
    • 4.4 重传次数限制
  • 五、滑动窗口机制
    • 5.1 为什么需要滑动窗口
    • 5.2 发送窗口
    • 5.3 接收窗口
    • 5.4 零窗口探测
    • 5.5 滑动窗口与send()的关系
    • 5.6 Reactor项目实战
  • 六、拥塞控制机制
    • 6.1 拥塞控制 vs 流量控制
    • 6.2 慢启动
    • 6.3 拥塞避免
    • 6.4 快速重传和快速恢复
    • 6.5 拥塞控制算法演进
    • 6.6 Linux内核源码分析
  • 七、面试高频题总结
  • 八、总结
  • 参考资料

一、前言

学完TCP三次握手和四次挥手后,我以为TCP就是这样了。但写Reactor项目的时候,遇到了更多问题:

  • send()函数有时候返回值小于发送的长度,为什么?
  • 客户端发送大量数据,服务器处理不过来,会发生什么?
  • TCP如何保证数据不丢、不乱、不重?
  • 什么是滑动窗口?拥塞窗口又是什么?

看王道视频的时候,我对这些概念有了基本认识。但真正理解,还是在写项目遇到问题、查资料、看Linux内核源码之后。特别是当我看到内核中tcp_write_xmit()函数如何决定发送多少数据时,才恍然大悟:原来TCP的可靠性和流量控制,全都在这里体现。

今天把这部分内容系统地整理出来,分享给大家。

本文包含:

  • TCP可靠性的三大机制(ACK确认、超时重传、滑动窗口)
  • 滑动窗口的完整工作流程(发送窗口、接收窗口、零窗口探测)
  • 拥塞控制的四个阶段(慢启动、拥塞避免、快速重传、快速恢复)
  • Linux内核源码分析(简化版,理解核心逻辑)
  • Reactor项目中如何处理send()recv()

二、TCP可靠性机制概述

TCP是可靠传输协议,建立在不可靠的IP层之上。IP层可能会丢包、乱序、重复,但TCP保证应用层看到的数据是有序的、不丢的、不重的。

TCP可靠性的三大支柱:

  1. 序列号(seq)和确认号(ack)

    • 每个字节都有序列号
    • 接收方通过ack确认收到了哪些数据
  2. 超时重传(RTO)

    • 发送方发送数据后启动定时器
    • 超时未收到ACK,重传数据
  3. 滑动窗口(流量控制)

    • 接收方告诉发送方自己能接收多少数据(rwnd)
    • 发送方根据rwnd控制发送速率

这三个机制协同工作,保证了TCP的可靠性。


三、ACK确认机制

3.1 序列号和确认号

TCP的每个字节都有序列号。这是我刚开始学习时最困惑的地方:为什么不是每个包有序列号,而是每个字节?

原因: TCP是面向字节流的,不关心"包"的概念。即使网络层把一个TCP报文分片了,TCP层也不管,只关心字节流。

发送方发送数据:字节流:H  e  l  l  o  W  o  r  l  d
序列号:100 101 102 103 104 105 106 107 108 109TCP报文:seq=100, len=5, data="Hello"seq=105, len=5, data="World"

确认号(ack)的含义: 期望接收的下一个字节序号

接收方收到seq=100, len=5的数据:收到了100, 101, 102, 103, 104这5个字节期望下一个字节是105发送:ACK, ack=105重要:ack=105表示"105之前的都收到了"

面试易错点: ack=500表示什么?

很多人答:收到了500号字节。错!

正确答案:收到了499号字节及之前的所有字节,期望接收500号字节。

3.2 累积确认

TCP使用累积确认机制:ack表示这个序号之前的所有字节都收到了。

发送方连续发送:seq=100, len=100  (100-199)seq=200, len=100  (200-299)seq=300, len=100  (300-399)接收方收到:seq=100, len=100 → 发送ACK, ack=200seq=200, len=100 → 发送ACK, ack=300seq=300, len=100 → 发送ACK, ack=400

优势: 减少ACK数量。如果每个字节都确认,开销太大。

问题: 中间丢包时,无法告诉发送方具体哪些收到了。

发送方发送:seq=100, len=100  (100-199)seq=200, len=100  (200-299) → 丢失seq=300, len=100  (300-399)接收方:收到seq=100 → 发送ACK, ack=200收到seq=300 → 但200-299没收到,只能发送ACK, ack=200发送方:收到ack=200收到ack=200 (重复)→ 无法知道300-399是否收到

解决方法:选择确认(SACK)。

3.3 选择确认(SACK)

SACK(Selective Acknowledgment)告诉发送方具体哪些数据收到了。

发送方发送:seq=100, len=100  (100-199)seq=200, len=100  (200-299) → 丢失seq=300, len=100  (300-399)seq=400, len=100  (400-499)接收方:收到seq=100 → ACK, ack=200收到seq=300 → ACK, ack=200, SACK=300-400 (300-399收到了)收到seq=400 → ACK, ack=200, SACK=300-400, 400-500发送方:ack=200:说明200-299没收到SACK=300-400, 400-500:说明300-499收到了→ 只需重传200-299

Linux中启用SACK:

# 查看
cat /proc/sys/net/ipv4/tcp_sack# 启用(默认已启用)
echo 1 > /proc/sys/net/ipv4/tcp_sack

3.4 捎带确认

TCP的ACK可以和数据一起发送,这叫捎带确认(Piggybacking)。

客户端和服务器交互:客户端 → 服务器:seq=100, len=10, data="GET /index.html"
服务器 → 客户端:seq=200, ack=110, len=1024, data="HTTP/1.1 200 OK..."↑                  ↑数据               捎带ACK不需要单独的ACK包

优势: 减少网络流量。

Nagle算法: 为了减少小包,TCP会等待一段时间,把多个小数据合并发送。但这会增加延迟。

TCP_NODELAY: 禁用Nagle算法,立即发送数据。

// 在Reactor项目中禁用Nagle算法
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

四、超时重传机制

4.1 超时重传的基本原理

发送方发送数据后,启动一个定时器。如果在规定时间内没收到ACK,就重传数据。

发送方:发送seq=100, len=100启动定时器,RTO=1秒1秒后,未收到ACK重传seq=100, len=100启动定时器,RTO=2秒 (指数退避)2秒后,未收到ACK重传seq=100, len=100启动定时器,RTO=4秒...

关键问题: RTO(Retransmission Timeout)设置多少?

  • 太短:网络正常但ACK延迟,导致不必要的重传
  • 太长:网络故障时,恢复太慢

4.2 RTO计算

RTO基于RTT(Round-Trip Time,往返时间)动态计算。

RTT测量:

发送数据:时刻T1
收到ACK:时刻T2
RTT = T2 - T1

RTO计算(RFC 6298):

SRTT(Smoothed RTT,平滑RTT):SRTT = (1-α) × SRTT + α × RTTα = 1/8RTTvar(RTT方差):RTTvar = (1-β) × RTTvar + β × |SRTT - RTT|β = 1/4RTO:RTO = SRTT + 4 × RTTvar最小值:200ms最大值:60秒

为什么要加4倍方差? 考虑网络抖动。如果RTT波动大,需要更长的超时时间。

面试要点: RTO不能固定,必须根据网络状况动态调整。

4.3 快速重传

如果发送方收到3个重复的ACK,不等超时,立即重传。

发送方发送:seq=100, len=100seq=200, len=100 → 丢失seq=300, len=100seq=400, len=100seq=500, len=100接收方:收到seq=100 → ACK, ack=200收到seq=300 → ACK, ack=200 (重复ACK #1)收到seq=400 → ACK, ack=200 (重复ACK #2)收到seq=500 → ACK, ack=200 (重复ACK #3)发送方:收到3个重复ACK (ack=200)立即重传seq=200, len=100不等超时

为什么是3个重复ACK? 因为网络可能乱序,1-2个重复ACK可能是乱序导致的。3个重复ACK基本可以确定是丢包。

面试易错点: “3个重复ACK"还是"4个ACK”?

答案:3个重复ACK,加上第1个正常ACK,总共4个。但我们说"收到3个重复ACK就重传"。

4.4 重传次数限制

TCP不会无限重传,有次数限制。

Linux参数:

# 查看
cat /proc/sys/net/ipv4/tcp_retries2# 默认15次

重传时间(指数退避):

第1次:1秒后
第2次:2秒后
第3次:4秒后
第4次:8秒后
第5次:16秒后
第6次:32秒后
第7次:64秒后
...
第15次:约16分钟后总时间:约30分钟

30分钟后: TCP连接终止,应用层收到错误。


五、滑动窗口机制

这是TCP中最难理解的部分。我看王道视频时似懂非懂,写项目时才真正理解。

5.1 为什么需要滑动窗口

停等协议(发送一个,等ACK,再发下一个)效率太低。

停等协议:时刻T0:发送数据1时刻T1:收到ACK1时刻T1:发送数据2时刻T2:收到ACK2...利用率 = 数据传输时间 / (数据传输时间 + RTT)如果RTT=100ms,传输时间=1ms利用率 = 1 / (1 + 100) ≈ 1%

滑动窗口: 允许发送方连续发送多个数据,不用等ACK。

滑动窗口:时刻T0:连续发送数据1, 2, 3, 4, 5时刻T1:收到ACK1, ACK2时刻T1:继续发送数据6, 7...利用率大幅提升

5.2 发送窗口

发送窗口把发送缓冲区分成4个部分:

发送缓冲区:
┌─────────┬─────────┬─────────┬─────────┐
│已发送已确认│已发送未确认│  可发送  │  不可发送 │
└─────────┴─────────┴─────────┴─────────┘↑                   ↑|<--发送窗口------->|

已发送已确认: 已经收到ACK的数据,可以从缓冲区删除。

已发送未确认: 已经发送,但还没收到ACK。如果丢包,需要重传。

可发送: 还没发送,但可以发送(接收方的rwnd允许)。

不可发送: 超出接收方的rwnd,不能发送。

窗口滑动:

初始状态:已发送已确认:0-99已发送未确认:100-199 (窗口大小=100)可发送:200-299收到ACK=150:已发送已确认:0-149 (窗口右移)已发送未确认:150-199可发送:200-349 (窗口扩大了50)

5.3 接收窗口

接收窗口表示接收方还能接收多少数据。

接收缓冲区:
┌─────────┬─────────┬─────────┐
│  已接收  │  可接收  │ 不可接收 │
│(应用层未读)│        │         │
└─────────┴─────────┴─────────┘|<--接收窗口-->|

rwnd(接收窗口大小): 接收方在ACK中告诉发送方。

接收方:接收缓冲区大小:8KB应用层已读取:2KB剩余空间:6KB发送ACK:ack=xxx, rwnd=6144

发送方:

收到rwnd=6144
发送窗口大小 = min(自己的拥塞窗口, 对方的rwnd)= min(cwnd, 6144)

5.4 零窗口探测

如果接收方的rwnd=0(缓冲区满了),发送方停止发送。但接收方的rwnd什么时候变大?发送方怎么知道?

零窗口探测(Zero Window Probe):

时刻T0:接收方:rwnd=0 (缓冲区满)发送方:收到rwnd=0,停止发送时刻T1:应用层读取数据,缓冲区有空间了接收方:rwnd=1024但如何通知发送方?TCP的解决方法:发送方定期发送"零窗口探测"报文- 包含1字节数据- 探测接收方的rwnd接收方回复ACK:ack=xxx, rwnd=1024发送方:收到rwnd=1024,继续发送

探测间隔: 指数退避,从5秒到60秒。

5.5 滑动窗口与send()的关系

这是写Reactor项目时必须理解的部分。

send()函数的行为:

int n = send(sockfd, buf, len, 0);返回值:n == len:数据全部拷贝到发送缓冲区0 < n < len:发送缓冲区满了,只拷贝了n字节n == -1, errno == EAGAIN:发送缓冲区满了,一个字节都没拷贝(非阻塞socket)

为什么会返回n < len?

场景:发送缓冲区大小:64KB已使用:60KB剩余:4KBsend(sockfd, buf, 10240, 0)  // 想发送10KB结果:只能拷贝4KB到发送缓冲区返回:n = 4096剩余6KB需要再次send

与滑动窗口的关系:

send() → 拷贝到发送缓冲区 → TCP协议栈根据滑动窗口发送TCP协议栈:实际发送窗口 = min(cwnd, rwnd)如果rwnd=0:- send()可能成功(拷贝到发送缓冲区)- 但TCP不会发送数据- 等待零窗口探测

5.6 Reactor项目实战

处理send()返回值:

// 错误的写法
void send_data(int sockfd, const char* data, int len) {send(sockfd, data, len, 0);  // 假设全部发送成功(错误)
}// 正确的写法
void send_data(int sockfd, const char* data, int len) {int total = 0;while (total < len) {int n = send(sockfd, data + total, len - total, 0);if (n > 0) {total += n;} else if (n == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 发送缓冲区满了,等待可写事件// 将剩余数据加入发送队列add_to_send_buffer(sockfd, data + total, len - total);break;} else {// 真实错误perror("send");break;}}}
}// 更好的方法:使用epoll的EPOLLOUT事件
void handle_write_event(int sockfd) {// 从发送队列取数据const char* data;int len;get_from_send_buffer(sockfd, &data, &len);int n = send(sockfd, data, len, 0);if (n > 0) {// 发送了n字节,更新队列remove_from_send_buffer(sockfd, n);if (send_buffer_empty(sockfd)) {// 队列空了,取消监听EPOLLOUTepoll_ctl(epollfd, EPOLL_CTL_MOD, sockfd, EPOLLIN | EPOLLET);}} else if (n == -1 && errno == EAGAIN) {// 还是满,继续等待EPOLLOUT}
}

六、拥塞控制机制

拥塞控制是TCP最复杂的部分。我看王道视频时只记住了慢启动、拥塞避免,但不理解为什么这样设计。后来看了Linux内核源码,才明白其中的智慧。

6.1 拥塞控制 vs 流量控制

很多人混淆这两个概念。

流量控制(Flow Control):

  • 目的:保护接收方
  • 机制:接收方通过rwnd告诉发送方自己能接收多少
  • 控制对象:单个连接的两端

拥塞控制(Congestion Control):

  • 目的:保护网络
  • 机制:发送方根据网络状况调整发送速率
  • 控制对象:整个网络

实际发送窗口:

send_window = min(cwnd, rwnd)cwnd:拥塞窗口(发送方自己维护)
rwnd:接收窗口(接收方告知)

6.2 慢启动

"慢启动"这个名字很容易误导人,其实一点都不慢,是指数增长。

原理:

cwnd初始值:1个MSS(Maximum Segment Size,最大报文段长度)每收到1个ACK:cwnd += 1结果:每个RTT,cwnd翻倍时刻T0:cwnd=1,发送1个包
时刻T1:收到1个ACK,cwnd=2,发送2个包
时刻T2:收到2个ACK,cwnd=4,发送4个包
时刻T3:收到4个ACK,cwnd=8,发送8个包
...1 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 256 → 512 → 1024

类比: 开车上高速,从20km/h开始,每秒翻倍:20 → 40 → 80 → 160。

为什么叫"慢启动"? 相对于一开始就发送大量数据,这算是"慢"的。

何时停止? 达到ssthresh(慢启动阈值)。

初始:cwnd=1, ssthresh=64(假设)cwnd < ssthresh:慢启动(指数增长)1 → 2 → 4 → 8 → 16 → 32 → 64cwnd >= ssthresh:拥塞避免(线性增长)64 → 65 → 66 → 67 → 68 → ...

6.3 拥塞避免

进入拥塞避免阶段后,cwnd线性增长。

原理:

每收到1个ACK:cwnd += 1/cwnd结果:每个RTT,cwnd += 1时刻T0:cwnd=64
时刻T1:cwnd=65 (收到64个ACK,cwnd += 64/64 = 1)
时刻T2:cwnd=66
时刻T3:cwnd=67
...

类比: 接近限速了,缓慢加速:160 → 161 → 162 → 163。

6.4 快速重传和快速恢复

发生丢包时(收到3个重复ACK),TCP认为网络拥塞了。

传统方法(TCP Tahoe):

收到3个重复ACK:ssthresh = cwnd / 2cwnd = 1重新慢启动

改进方法(TCP Reno):

收到3个重复ACK:ssthresh = cwnd / 2cwnd = ssthresh + 3快速恢复(不回到慢启动)每收到1个重复ACK:cwnd += 1(膨胀窗口)收到新ACK:cwnd = ssthresh进入拥塞避免

示例:

时刻T0:cwnd=16, ssthresh=32
时刻T1:收到3个重复ACK(丢包)ssthresh = 16 / 2 = 8cwnd = 8 + 3 = 11快速重传丢失的包时刻T2:收到第4个重复ACKcwnd = 12时刻T3:收到第5个重复ACKcwnd = 13时刻T4:收到新ACK(重传的包确认了)cwnd = 8进入拥塞避免

为什么要膨胀窗口? 每个重复ACK说明有一个包离开了网络,可以发送新包。

超时重传: 比3个重复ACK更严重,说明网络严重拥塞。

超时:ssthresh = cwnd / 2cwnd = 1重新慢启动

6.5 拥塞控制算法演进

TCP Tahoe(1988):

  • 慢启动、拥塞避免
  • 丢包时回到慢启动

TCP Reno(1990):

  • 加入快速重传、快速恢复
  • 丢包时不一定回到慢启动

TCP New Reno(1999):

  • 改进快速恢复,处理多个丢包

TCP Cubic(2005,Linux默认):

  • 改进cwnd增长函数,使用三次函数
  • 更快达到之前的cwnd
  • 更适合高带宽网络

TCP BBR(2016,Google):

  • 不基于丢包,而是基于带宽和RTT
  • 更适合现代网络

查看当前算法:

# Linux
cat /proc/sys/net/ipv4/tcp_congestion_control# 设置为cubic
echo cubic > /proc/sys/net/ipv4/tcp_congestion_control

6.6 Linux内核源码分析

这部分是为了加深理解,不是让你记住代码,而是理解原理。

简化的内核数据结构:

struct tcp_sock {u32 snd_cwnd;      // 拥塞窗口u32 snd_ssthresh;  // 慢启动阈值u32 snd_wnd;       // 接收方的rwnd// ...
};// 实际发送窗口
static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
{return tp->snd_una + min(tp->snd_wnd, tp->snd_cwnd);
}

慢启动实现(简化):

// net/ipv4/tcp_cong.c// 慢启动:每个ACK,cwnd += 1
void tcp_slow_start(struct tcp_sock *tp, u32 acked)
{tp->snd_cwnd += acked;// 限制cwnd不超过ssthreshtp->snd_cwnd = min(tp->snd_cwnd, tp->snd_ssthresh);
}

拥塞避免实现(简化):

// 拥塞避免:每个RTT,cwnd += 1
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w)
{if (tp->snd_cwnd_cnt >= w) {tp->snd_cwnd++;tp->snd_cwnd_cnt = 0;} else {tp->snd_cwnd_cnt++;}
}

快速重传处理(简化):

// 收到3个重复ACK
void tcp_fastretrans_alert(struct sock *sk, int pkts_acked)
{struct tcp_sock *tp = tcp_sk(sk);if (tp->dup_acks >= 3) {// 进入快速恢复tp->snd_ssthresh = max(tp->snd_cwnd / 2, 2);tp->snd_cwnd = tp->snd_ssthresh + 3;// 快速重传tcp_retransmit_skb(sk, tcp_write_queue_head(sk));}
}

发送数据的核心函数(简化):

// net/ipv4/tcp_output.cstatic bool tcp_write_xmit(struct sock *sk, unsigned int mss_now,int nonagle, int push_one, gfp_t gfp)
{struct tcp_sock *tp = tcp_sk(sk);while (1) {// 计算可发送窗口u32 limit = tcp_wnd_end(tp) - tp->snd_nxt;if (limit <= 0)break;  // 窗口满了,不能发送// 发送数据if (tcp_transmit_skb(sk, skb, 1, gfp))break;}return true;
}

这就是TCP发送数据的核心逻辑:检查窗口(min(cwnd, rwnd)),有空间就发送。


七、面试高频题总结

必背知识点(10个)

  1. TCP可靠性的三大机制

    • 序列号和确认号
    • 超时重传
    • 滑动窗口
  2. ack=500表示什么?

    • 收到了499及之前的所有字节
    • 期望接收500号字节
  3. 快速重传的条件

    • 收到3个重复ACK
    • 不等超时,立即重传
  4. 滑动窗口的作用

    • 流量控制
    • 提高传输效率
  5. rwnd和cwnd的区别

    • rwnd:接收窗口,保护接收方
    • cwnd:拥塞窗口,保护网络
    • 实际窗口 = min(rwnd, cwnd)
  6. 慢启动的过程

    • 指数增长:1 → 2 → 4 → 8 → 16
    • 达到ssthresh后进入拥塞避免
  7. 拥塞避免的过程

    • 线性增长:每个RTT增加1个MSS
    • cwnd += 1 / cwnd
  8. 快速恢复的过程

    • ssthresh = cwnd / 2
    • cwnd = ssthresh + 3
    • 不回到慢启动
  9. 零窗口探测

    • rwnd=0时,发送方停止发送
    • 定期发送1字节探测
  10. send()返回值的处理

    • 可能小于len,需要循环发送
    • 非阻塞时可能返回EAGAIN

面试标准答案模板

问:TCP如何保证可靠性?

TCP通过三大机制保证可靠性:

  1. 序列号和确认号: 每个字节都有序列号,接收方通过ACK确认收到的数据。ack表示期望接收的下一个字节序号。

  2. 超时重传: 发送方发送数据后启动定时器,如果RTO时间内未收到ACK,就重传数据。RTO基于RTT动态计算。

  3. 滑动窗口: 接收方通过rwnd告诉发送方自己能接收多少数据,发送方根据rwnd和cwnd控制发送速率。

此外,TCP还有快速重传机制:收到3个重复ACK时,不等超时,立即重传丢失的数据。

在我的Reactor项目中,我需要处理send()的返回值,因为发送缓冲区可能满了,send()返回值小于len,需要循环发送剩余数据。

问:拥塞控制和流量控制的区别?

流量控制是保护接收方,拥塞控制是保护网络。

流量控制:接收方通过rwnd(接收窗口)告诉发送方自己能接收多少数据。如果接收缓冲区满了,rwnd=0,发送方停止发送。

拥塞控制:发送方根据网络状况调整cwnd(拥塞窗口)。包括慢启动(指数增长)、拥塞避免(线性增长)、快速重传和快速恢复。

实际发送窗口 = min(cwnd, rwnd),同时考虑了流量控制和拥塞控制。

Linux默认使用TCP Cubic算法,相比传统的Reno算法,更适合高带宽网络。


八、总结

TCP的可靠性和拥塞控制是计网最难也最重要的部分。学习这部分内容,我的体会是:

理论学习:

  1. 先理解基本概念:ACK、重传、滑动窗口
  2. 画图理解:窗口如何滑动、cwnd如何变化
  3. 理解"为什么":为什么是指数增长?为什么要慢启动?

实践验证:

  1. 写代码处理send()的返回值
  2. 用tcpdump抓包,观察真实的窗口变化
  3. 压力测试,观察cwnd的动态调整

面试准备:

  1. 必须理解rwnd和cwnd的区别
  2. 必须能画出慢启动和拥塞避免的曲线
  3. 结合项目讲解如何处理send()

下一篇文章,我们聊HTTP/1.1的新特性:持久连接、缓存机制、分块传输。


参考资料

  • 王道考研《计算机网络》视频课程
  • RFC 5681:TCP Congestion Control
  • RFC 6298:Computing TCP’s Retransmission Timer
  • Linux内核源码:net/ipv4/tcp_output.cnet/ipv4/tcp_input.c
  • 《TCP/IP详解 卷1:协议》第17-21章

http://www.dtcms.com/a/566488.html

相关文章:

  • 宝塔做网站安全吗做网站龙华
  • safetensors转为gguf,并在ollama中部署
  • 做二手车按揭的网站艺术培训机构
  • 如何给网站做右侧导航互联网网络推广公司
  • 公司网站优化推广宁波企业名称查询网站
  • 做淘宝客网站服务器高新网站建设
  • Mysql 读书笔记
  • 网上做任务佣金高的网站wordpress付费浏览
  • Flutter---卡片交换器
  • MAC-SQL 算法一
  • 大连爱得科技网站建设公司怎么样在线设计平台都有哪些比较好用的
  • 【2051】【例3.1】偶数
  • 北京网站开发外包做网站看什么书
  • 怎么做网站推广临沂世界网站
  • C# 使用XML文件保存配方数据
  • 小说网站自主建设网站域名申请
  • 西安谁家的集团门户网站建设比较好上海公司车牌
  • Spring配置数据源
  • Product Hunt 每日热榜 | 2025-11-02
  • 基于图像的三维重建
  • 越秀区做网站河南网站建设价格与方案
  • 什么网站的新闻做参考文献中信建设有限责任公司属于央企吗
  • 硬件工程师-基础知识(一)
  • 都匀经济开发区建设局网站无锡电子商务网站制作
  • html5 input[type=date]如何让日期中的年/月/日改成英文
  • 嘉兴城乡建设局网站株洲seo优化哪家好
  • 【开题答辩全过程】以 法律类教辅平台为例,包含答辩的问题和答案
  • 商务网站建设哪家好免费聊天不充值软件
  • 网站 用cms 侵权免费的网站域名查询565wcc
  • 群晖 NAS 办公套件:用Synology Calendar 高效管理日程与任务