通信:(2) TCP/UDP、流量/拥塞控制、ARP 与 Socket 应用

1. TCP

1.1 TCP 报文结构

TCP报文由首部(20~60字节) + 数据(MSS)组成,首部字段实现可靠传输。

TCP 首部结构如下表所示:

字段 长度 作用
源端口/目的端口 2B/2B 标识通信两端的应用进程(如HTTP=80)
序列号(Sequence Number, seq) 4B 标记数据部分第一个字节 的位置(不一定从0开始
确认号(Acknowledgment Number, ack) 4B 期望收到的下一个序列号
标志位(6bit) --- 控制连接状态
- URG 1bit 紧急指针有效(如HTTP紧急请求)
- ACK 1bit 确认号字段有效(正常通信中几乎总是1)
- PSH 1bit 请求立即交付数据(如HTTP短请求)
- RST 1bit 重置连接(如端口未开放)
- SYN 1bit 同步序列号(建立连接时使用)
- FIN 1bit 结束连接
窗口(Window Size) 2B 接收方通告的接收窗口大小,用于流量控制
检验和 2B 校验首部+数据的完整性
紧急指针 2B 配合URG使用,指向紧急数据末尾
  • 序列号不是"报文编号",而是每个字节的编号(例如一个1KB报文,序列号范围是1000~1999)。
  • 确认号=已收到的最后一个字节+1(如收到1000~1999,确认号=2000)。
  • 窗口字段是流量控制的核心,接收方通过它告诉发送方"我还能处理多少数据"。

1.2 TCP 的可靠传输

1. 序列号 + 确认应答

  • 发送方为每个字节分配序列号,接收方通过确认号反馈已收到的数据范围。
  • 示例:发送方发送序列号100~199,接收方回复确认号200 → 表示100~199已正确接收。

2. 超时重传

  • 发送方发出数据后启动计时器,若超时未收到确认,则重传数据。
  • 关键:超时时间需动态调整(基于RTT测量),避免过短导致频繁重传,过长降低效率。

3. 数据校验

  • 通过检验和检测传输错误,出错数据会被丢弃并触发重传。

流程:发送数据 → 等待确认 → 超时重传/确认接收 → 继续发送

1.3 TCP 的流量控制

流量控制的核心是滑动窗口机制,通过动态调整窗口大小匹配接收方处理能力。

窗口 说明
接收窗口 接收方缓冲区剩余空间,通过TCP首部的"窗口"字段告知发送方
发送窗口 发送方实际发送量 = min(接收窗口, 拥塞窗口)

动态调整过程:

  1. 接收方初始窗口=4000字节 → 发送方发送4000字节。
  2. 接收方处理了2000字节 → 通过ACK更新窗口=2000字节。
  3. 发送方后续只能发送2000字节,避免接收方缓冲区溢出。

1.4 TCP 的拥塞控制

拥塞控制通过调整发送速率避免网络过载,核心算法包括:

1.4.1 慢开始(Slow Start)

  • 初始拥塞窗口(cwnd)=1 MSS,每收到一个ACK,cwnd翻倍(指数增长)。
  • 触发条件:连接刚建立或超时重传后。
  • 退出条件:cwnd ≥ 慢开始阈值(ssthresh)。

1.4.2 拥塞避免(Congestion Avoidance)

  • cwnd 线性增长(每RTT增加1 MSS),避免指数增长导致拥塞。

1.4.3 快重传(Fast Retransmit)

  • 发送方收到3个重复ACK(如连续收到ACK=1000的3次),立即重传丢失的报文(无需等待超时)。
  • 优势:比超时重传快1~2个RTT,显著降低延迟。

1.4.4 快恢复(Fast Recovery)

  • 触发快重传后:
    • ssthresh = cwnd / 2
    • cwnd = ssthresh + 3 MSS
    • 跳过慢开始,直接进入拥塞避免。
  • 特殊情况:若超时重传,则ssthresh = cwnd / 2,cwnd重置为1 MSS,重新慢开始。

1.5 TCP 的三次握手

握手①和握手②不能携带数据,而握手③可以选择是否携带数据。

握手③不携带数据:

握手③携带数据:

1.6 TCP 的四次挥手

如图:

注意:

当服务器在收到客户端的FIN后,如果没有数据需要继续发送,它可以立刻将第二次挥手(ACK)和第三次挥手(自己的FIN)合并成一个报文(FIN+ACK)发回给客户端。

2. UDP

2.1 UDP 的首部

UDP报文由固定8字节首部 + 数据组成,是传输层最轻量的协议。

字段 长度 作用 关键细节
源端口 2B 标识发送方应用进程 可选字段(值为0表示不使用)
目的端口 2B 标识接收方应用进程 如DNS=53,NTP=123
长度 2B 首部+数据总长度(最小8字节) 用于界定报文边界
检验和 2B 校验首部+数据完整性 IPv4中可选,IPv6中强制要求

2.2 UDP 的无连接与不可靠传输

  • 不建立状态:发送前无需三次握手,接收方无需维护连接状态
  • 每个报文独立:报文间无关联,丢失一个不影响后续传输
  • 无连接释放:无需四次挥手,应用层直接停止发送
不可靠类型 说明 影响
不保证送达 无确认/重传机制,丢包即丢失 视频直播丢1帧 → 花屏但不卡顿
不保证顺序 无序列号,后发报文可能先到 VoIP中0.1秒乱序 → 人耳几乎无感
不保证完整性 检验和可选(IPv4),错误数据可能被接收 应用层需自行校验(如DNS使用应用层校验)

3. ARP

ARP工作在数据链路层中, 在局域网中, 设备间的通信依赖于 MAC 地址(物理地址),但上层协议(如 IP 协议)使用的是 IP 地址。ARP (Address Resolution Protocol,地址解析协议) 包的作用就是在这两种地址之间建立映射关系:

  • 当主机需要与同一局域网内的另一台主机通信时,会先检查本地 ARP 缓存(存储 IP 与 MAC 的映射表)。
  • 若缓存中没有目标 IP 对应的 MAC 地址,就会发送ARP 请求包(广播形式),拥有该 IP 地址的目标主机会收到请求,并返回ARP 应答包(单播形式),告知自己的 MAC 地址。
  • 发送方收到应答后,将 IP 与 MAC 的映射存入 ARP 缓存,后续通信可直接使用该 MAC 地址。

4. Socket 应用

4.1 Socket 代码调用流程

步骤 功能 系统调用 内核行为 关键注意事项
1 创建Socket socket(AF_INET, SOCK_STREAM, 0) • 分配文件描述符 • 创建struct sock • 初始化TCP协议栈 • 返回值≥0为有效fd • protocol=0自动匹配TCP
2 设置监听选项 setsockopt(SO_REUSEADDR) • 设置sk->sk_reuse=1 • 允许绑定TIME_WAIT状态端口 必须 在bind前设置 • 生产环境建议同时设置SO_REUSEPORT
3 绑定地址 bind(listenfd, ...) • 检查端口冲突 • 插入tcp_listening_hash • 为INADDR_ANY创建多接口监听 htonl(INADDR_ANY)=0.0.0.0 • 端口需在1024-65535(非特权端口)
4 开始监听 listen(listenfd, 1024) • 初始化SYN队列(tcp_max_syn_backlog) • 初始化Accept队列(min(1024,somaxconn) backlog实际值受net.core.somaxconn限制 • 队列溢出导致SYN包丢失
5 接受连接 `accept4(..., SOCK_NONBLOCK SOCK_CLOEXEC)` • 从Accept队列移除连接 • 创建新socket(connfd) • 状态从LISTEN→ESTABLISHED
6 连接优化 setsockopt(SO_KEEPALIVE) setsockopt(TCP_NODELAY) • 启用TCP保活探测(2h后开始) • 禁用Nagle算法(立即发送小包) TCP_NODELAY适合实时应用 • 保活参数需调优:tcp_keepalive_time
7 读取数据 read(connfd, buf, 1024) • 检查接收缓冲区 • 非阻塞模式下无数据立即返回 非阻塞关键EAGAIN表示无数据 • 需配合epoll实现高效等待
8 处理数据 // 处理逻辑 用户空间业务逻辑 • 需处理粘包/半包问题(本例未实现) • 实际应用需协议解析
9 写回数据 write(connfd, buf, n) • 检查发送缓冲区空间 • 触发TCP发送流程 • 非阻塞模式下可能部分写入 • 需循环写入直到全部发送
10 关闭连接 close(connfd)shutdown(SHUT_WR) • 发送FIN包(四次挥手开始) • 进入TIME_WAIT状态(主动关闭方) 最佳实践 :让客户端主动关闭(避免服务器TIME_WAIT堆积) • shutdown(SHUT_WR)实现半关闭
11 终止服务 close(listenfd) • 释放监听socket资源 • 从哈希表移除监听项 • 已建立的连接不受影响(由connfd管理) • 新连接请求将被拒绝

4.2 代码示例

cpp 复制代码
    // 1. 创建socket

    /*

    * @function: int socket(int domain, int type, int protocol);

    * @param: domain: 协议族

    *           - AF_INET: IPv4

    *           - AF_INET6: IPv6

    * @param: type: 套接字类型

    *           - SOCK_STREAM: 流式套接字(TCP)

    *           - SOCK_DGRAM: 数据报套接字(UDP)

    * @param: protocol: 协议类型

    *           - 0: 根据前两个参数自动选择

    *           - IPPROTO_TCP: TCP协议

    *           - IPPROTO_UDP: UDP协议

    * @return: 成功返回文件描述符, -1 失败

    */

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    if(listenfd < 0){

        LOG_ERROR("socket() error: %s", strerror(errno));

        std::exit(-1);

    }



    // 2. 设置套接字选项(可选)

    /*

    * @function: int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

    * @param: sockfd: 套接字文件描述符

    * @param: level: 选项级别

    *           - SOL_SOCKET: 通用套接字选项

    *           - IPPROTO_TCP: TCP协议选项

    * @param: optname: 选项名称

    *           - SO_REUSEADDR: 允许重用本地地址

    *           - SO_REUSEPORT: 允许重用本地端口

    *           - TCP_NODELAY: 禁用Nagle算法

    * @param: optval: 选项值指针

    * @param: optlen: 选项值长度

    * @return: 0 成功, -1 失败

    */

    int opt = 1;

    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));



    // 3. 绑定地址和端口

    /*

    * @function: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    * @param: sockfd: 套接字文件描述符

    * @param: addr: 指向要绑定的地址结构体指针

    * @param: addrlen: 地址结构体长度

    * @return: 0 成功, -1 失败

    */

    struct sockaddr_in servaddr;

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡

    servaddr.sin_port = htons(8888);              // 监听8888端口



    if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){

        LOG_ERROR("bind() error: %s", strerror(errno));

        std::exit(-1);

    }



    // 4. 开始监听

    /*

    * @function: int listen(int sockfd, int backlog);

    * @param: sockfd: 套接字文件描述符

    * @param: backlog: 最大连接请求队列长度

    * @return: 0 成功, -1 失败

    */

    if(listen(listenfd, 1024) < 0){

        LOG_ERROR("listen() error: %s", strerror(errno));

        std::exit(-1);

    }



    // 5. 接受客户端连接

    /*

    * @function: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    * @function: int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

    * @param: sockfd: 监听套接字文件描述符

    * @param: addr: 输出参数,保存客户端地址信息

    * @param: addrlen: 输入输出参数,地址结构体长度

    * @param: flags: 标志位

    *           - SOCK_NONBLOCK: 设置非阻塞

    *           - SOCK_CLOEXEC: 执行exec时关闭

    * @return: 成功返回新连接的文件描述符, -1 失败

    */

    struct sockaddr_in cliaddr;

    socklen_t cliaddr_len = sizeof(cliaddr);

    int connfd = accept4(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len, SOCK_NONBLOCK | SOCK_CLOEXEC);

    if(connfd < 0){

        LOG_ERROR("accept() error: %s", strerror(errno));

        continue; // 非致命错误,继续处理

    }



    // 6. 设置新连接套接字选项(可选)

    int keepalive = 1;

    setsockopt(connfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));



    int nodelay = 1;

    setsockopt(connfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));



    // 7. 读写数据

    /*

    * @function: ssize_t read(int fd, void *buf, size_t count);

    * @function: ssize_t write(int fd, const void *buf, size_t count);

    * @param: fd: 文件描述符

    * @param: buf: 数据缓冲区

    * @param: count: 要读取/写入的字节数

    * @return: 成功返回实际读取/写入的字节数, -1 失败

    */

    char buf[1024];

    ssize_t n = read(connfd, buf, sizeof(buf));

    if(n < 0){

        if(errno == EAGAIN || errno == EWOULDBLOCK){

            // 非阻塞模式下没有数据可读

            continue;

        }

        LOG_ERROR("read() error: %s", strerror(errno));

        close(connfd);

        continue;

    }else if(n == 0){

        // 客户端关闭连接

        LOG_INFO("client closed connection");

        close(connfd);

        continue;

    }



    // 处理数据

    // ...



    // 写回数据

    write(connfd, buf, n);



    // 8. 关闭连接

    /*

    * @function: int close(int fd);

    * @function: int shutdown(int sockfd, int how);

    * @param: fd: 文件描述符

    * @param: how: 关闭方式

    *           - SHUT_RD: 关闭读端

    *           - SHUT_WR: 关闭写端

    *           - SHUT_RDWR: 关闭读写端

    * @return: 0 成功, -1 失败

    */

    close(connfd);

    // 或者优雅关闭

    shutdown(connfd, SHUT_WR);



    // 9. 关闭监听套接字

    close(listenfd);
相关推荐
近津薪荼2 小时前
优选算法——前缀和(6):和可被 K 整除的子数组
c++·算法
草莓熊Lotso2 小时前
Linux 磁盘基础:从物理结构到 CHS/LBA 寻址,吃透数据存储底层逻辑
linux·运维·服务器·c++·人工智能
燃于AC之乐2 小时前
深入解剖STL map/multimap:接口使用与核心特性详解
开发语言·c++·stl·面试题·map·multimap
草莓熊Lotso2 小时前
Qt 核心事件系统全攻略:鼠标 / 键盘 / 定时器 / 窗口 + 事件分发与过滤
运维·开发语言·c++·人工智能·qt·ui·计算机外设
阿kun要赚马内4 小时前
C++中的Windows API双缓冲技术
c++
WBluuue10 小时前
Codeforces 1078 Div2(ABCDEF1)
c++·算法
学无止境_永不停歇11 小时前
十、C++多态
开发语言·c++
byzh_rc11 小时前
[深度学习网络从入门到入土] 拓展 - Inception
网络·人工智能·深度学习
老歌老听老掉牙11 小时前
QT开发踩坑记:按钮点击一次却触发两次?深入解析信号槽自动连接机制
c++·qt