1. 什么是高可靠通信?
高可靠通信是指在通信过程中,数据能够无丢失、无重复、无乱序、无损坏地从发送方传递到接收方,并且能够应对网络丢包、延迟、乱序等异常情况。
典型实现:TCP 协议,通过确认应答、超时重传、序号机制、校验和等方式保证可靠性。
2. TCP 与 UDP 的区别有哪些?
| 对比项 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手建立连接) | 无连接 |
| 可靠性 | 可靠传输(确认、重传、校验) | 不可靠传输(不保证到达) |
| 有序性 | 保证数据按序到达 | 不保证有序 |
| 流量控制 | 支持滑动窗口流量控制 | 不支持 |
| 拥塞控制 | 支持拥塞控制 | 不支持 |
| 适用场景 | 文件传输、网页浏览、邮件 | 直播、语音通话、游戏 |
3. 简述三种超时检测的办法
- 固定超时重传:设置固定的超时时间,超时未收到 ACK 则重传。实现简单,但无法适应网络变化,容易出现过早或过晚重传。
- 自适应超时重传(RTO):基于往返时间(RTT)动态计算超时时间,公式:
RTO = SRTT + 4 * RTTvar,能更好地适应网络波动。 - ** Karn 算法 **:不使用重传报文的 RTT 样本,避免 RTT 测量不准导致的超时计算错误,配合自适应 RTO 使用。
4. 什么是粘包?如何解决粘包问题?
- 粘包:TCP 是字节流协议,没有消息边界,多个独立的数据包可能被合并成一个发送,导致接收方一次读取多个消息,造成数据 “粘在一起”。
- 解决方法:
- 定长消息:每个消息固定长度,不足部分填充占位符。
- 特殊分隔符:在消息末尾添加约定的分隔符(如
\r\n),接收方按分隔符拆分。 - 消息头 + 消息体:消息头包含消息总长度,接收方先读头再按长度读体(工业界最常用)。
5. OSI 七层协议模型及各层功能
- 物理层:传输比特流,定义物理介质的电气、机械特性。
- 数据链路层:成帧、差错检测、MAC 寻址,如以太网、PPP 协议。
- 网络层:路由选择、IP 寻址、拥塞控制,如 IP、ICMP 协议。
- 传输层:端到端通信、流量控制、可靠性保证,如 TCP、UDP 协议。
- 会话层:建立、管理和终止会话连接。
- 表示层:数据格式转换、加密解密、压缩解压缩。
- 应用层:为应用程序提供网络服务,如 HTTP、FTP、DNS 协议。
6. TCP/IP 四层协议族及各层协议
- 网络接口层:以太网、PPP、ARP、RARP
- 网络层:IP、ICMP、IGMP
- 传输层:TCP、UDP
- 应用层:HTTP、FTP、SMTP、DNS、Telnet、SSH
7. TCP 服务器与客户端代码框架(仅函数名)
- 服务器:
socket()→bind()→listen()→accept()→recv()/send()→close() - 客户端:
socket()→connect()→send()/recv()→close()
8. TCP 如何判断对方异常断电?
- 异常断电:对方主机突然断电,不会发送 FIN 包。
- 判断方法:
- 超时重传失败:发送方多次重传数据后仍未收到 ACK,判定连接断开。
- 保活机制(SO_KEEPALIVE):开启 TCP 保活,定时发送探测包,多次无响应则判定连接失效。
- 应用层心跳包:应用层定时发送心跳,超时未收到则判定断开。
9. 二层交换机的工作原理
- 学习:收到数据帧时,记录源 MAC 地址和对应端口,构建 MAC 地址表。
- 转发:根据目的 MAC 地址查找 MAC 地址表,转发到对应端口。
- 广播:如果目的 MAC 不在地址表中,向除接收端口外的所有端口广播。
- 过滤:同一端口收到的帧不转发,避免环路。
10. 两种并发服务器设计及优缺点
- 多进程服务器:
- 原理:每来一个连接,
fork()一个子进程处理。 - 优点:进程隔离,一个连接崩溃不影响其他连接。
- 缺点:进程创建 / 切换开销大,连接数多时性能差。
- 原理:每来一个连接,
- 多线程服务器:
- 原理:每来一个连接,创建一个线程处理。
- 优点:线程开销比进程小,切换快。
- 缺点:线程共享地址空间,一个线程崩溃会导致整个进程退出;需处理同步问题。
11. 三种 IO 多路复用方法及特点
- select:
- 特点:支持跨平台,监听的文件描述符数量有限(默认 1024),每次调用需遍历所有描述符,效率低。
- poll:
- 特点:无文件描述符数量限制,但同样需要遍历,效率和 select 相近。
- epoll:
- 特点:Linux 独有,基于事件驱动,仅返回就绪的文件描述符,支持水平 / 边缘触发,高并发场景效率极高。
12. 五种 IO 模型及同步 / 异步
- 阻塞 IO:同步 IO,
recv()会一直阻塞直到数据到达。 - 非阻塞 IO:同步 IO,调用
recv()立即返回,数据未就绪则返回错误,需轮询。 - IO 多路复用(select/poll/epoll):同步 IO,由内核监听多个文件描述符,数据就绪后通知用户进程。
- 信号驱动 IO:同步 IO,内核数据就绪时发送信号通知进程。
- 异步 IO(AIO):异步 IO,用户进程发起 IO 请求后立即返回,内核完成 IO 后通知进程。
13. 路由器的工作原理及数据包发送流程
- 工作原理:路由器根据路由表,选择最佳路径转发数据包,实现不同网络间的通信。
- 数据包发送流程:
- 主机判断目标 IP 是否在同一网段,若不在则发送给默认网关(路由器)。
- 路由器收到数据包,根据目标 IP 查找路由表,选择下一跳。
- 将数据包转发到下一跳路由器,重复步骤 2,直到到达目标网络。
- 目标路由器将数据包转发给目标主机。
14. TCP 三次握手与四次挥手
三次握手(建立连接)
- 客户端发送 SYN 包(seq=x)到服务器,进入 SYN_SENT 状态。
- 服务器收到 SYN,回复 SYN+ACK 包(seq=y, ack=x+1),进入 SYN_RCVD 状态。
- 客户端收到 SYN+ACK,回复 ACK 包(ack=y+1),双方进入 ESTABLISHED 状态。
四次挥手(断开连接)
- 客户端发送 FIN 包(seq=u),进入 FIN_WAIT1 状态。
- 服务器收到 FIN,回复 ACK 包(ack=u+1),进入 CLOSE_WAIT 状态,客户端进入 FIN_WAIT2 状态。
- 服务器发送完数据后,发送 FIN 包(seq=v),进入 LAST_ACK 状态。
- 客户端收到 FIN,回复 ACK 包(ack=v+1),进入 TIME_WAIT 状态,服务器收到 ACK 后进入 CLOSED 状态。
编写 epoll 多路复用 TCP 服务器代码(完整示例)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #define MAX_EVENTS 1024 #define LISTEN_PORT 8888 int main() { int listen_fd, epoll_fd, nfds; struct sockaddr_in server_addr; struct epoll_event events[MAX_EVENTS], event; // 1. 创建监听socket listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); exit(1); } // 2. 绑定地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(LISTEN_PORT); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(1); } // 3. 监听 if (listen(listen_fd, 128) < 0) { perror("listen"); exit(1); } // 4. 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd < 0) { perror("epoll_create1"); exit(1); } // 5. 注册监听socket事件 event.events = EPOLLIN; event.data.fd = listen_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) < 0) { perror("epoll_ctl add listen"); exit(1); } printf("Server listening on port %d...\n", LISTEN_PORT); // 6. 事件循环 while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds < 0) { perror("epoll_wait"); break; } for (int i = 0; i < nfds; i++) { int fd = events[i].data.fd; // 处理新连接 if (fd == listen_fd) { struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len); if (client_fd < 0) { perror("accept"); continue; } printf("New connection: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 将客户端fd加入epoll event.events = EPOLLIN | EPOLLET; // 边缘触发 event.data.fd = client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event); } // 处理客户端数据 else { char buf[1024] = {0}; ssize_t ret = recv(fd, buf, sizeof(buf)-1, 0); if (ret <= 0) { // 客户端断开或错误 printf("Client disconnected, fd=%d\n", fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); close(fd); } else { printf("Recv from fd=%d: %s\n", fd, buf); send(fd, buf, ret, 0); // 回显数据 } } } } close(listen_fd); close(epoll_fd); return 0; }