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(接收窗口, 拥塞窗口) |
动态调整过程:
- 接收方初始窗口=4000字节 → 发送方发送4000字节。
- 接收方处理了2000字节 → 通过ACK更新窗口=2000字节。
- 发送方后续只能发送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);