一、TCP/IP 基础概念(必问)
这部分是面试的 "打底题",考察对网络基础的理解,常以问答形式出现:
1. TCP/IP 协议栈分层
| TCP/IP 4 层模型 | 对应 OSI 7 层 | 核心协议 / 功能 |
|---|---|---|
| 应用层 | 应用层 + 表示层 + 会话层 | HTTP/FTP/Telnet,定义应用交互规则 |
| 传输层 | 传输层 | TCP/UDP,端到端数据传输(端口标识) |
| 网络层 | 网络层 | IP/ICMP/ARP,跨网络路由(IP 地址标识) |
| 链路层 | 数据链路层 + 物理层 | 以太网 / PPP,局域网内帧传输(MAC 地址) |
2. TCP vs UDP 核心区别(高频)
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认 / 重传 / 排序) | 不可靠(无确认) |
| 传输方式 | 面向字节流 | 面向数据报 |
| 拥塞 / 流量控制 | 支持(滑动窗口 / 拥塞控制) | 不支持 |
| 头部开销 | 20-60 字节 | 8 字节 |
| 适用场景 | 文件传输 / HTTP / 邮件 | 视频 / 语音 / 直播 / 游戏 |
3. 端口范围
- 知名端口:0~1023(如 80=HTTP,443=HTTPS,22=SSH);
- 注册端口:1024~49151;
- 动态 / 私有端口:49152~65535(客户端随机使用)。
二、TCP 核心原理(面试重点)
这部分是考察深度的核心,常问 "为什么" 和 "过程":
1. TCP 三次握手(为什么需要三次?)
过程(服务端先listen):
- 客户端 → 服务端:SYN(同步序列号),客户端进入
SYN_SENT状态; - 服务端 → 客户端:SYN+ACK(确认客户端 SYN,同步自己的序列号),服务端进入
SYN_RCVD状态; - 客户端 → 服务端:ACK(确认服务端 SYN),双方进入
ESTABLISHED状态。
核心问题:为什么不是两次?
- 两次握手无法确认 "客户端能收到服务端的报文",可能导致服务端为无效客户端分配资源(半连接队列溢出);
- 三次握手能确保双方收发能力都正常,且完成序列号同步。
2. TCP 四次挥手(为什么需要四次?)
过程:
- 主动关闭方 → 被动方:FIN(请求关闭),主动方进入
FIN_WAIT1; - 被动方 → 主动方:ACK(确认 FIN),被动方进入
CLOSE_WAIT,主动方进入FIN_WAIT2; - 被动方 → 主动方:FIN(被动方数据发送完毕,请求关闭),被动方进入
LAST_ACK; - 主动方 → 被动方:ACK(确认 FIN),主动方进入
TIME_WAIT,被动方收到后进入CLOSED; - 主动方等待
2MSL(报文最大生存时间)后进入CLOSED。
核心问题:为什么不是三次?
- TCP 是全双工通信,关闭时需要分别关闭 "发送" 和 "接收" 通道,被动方收到 FIN 后可能还有数据要发,因此需要先 ACK 确认关闭,等数据发完再发 FIN。
3. TIME_WAIT 状态(高频)
- 作用:① 确保最后一个 ACK 到达(若被动方没收到 ACK,会重发 FIN);② 避免旧连接的延迟数据包干扰新连接(2MSL 可让旧数据包失效)。
- 问题:TIME_WAIT 过多会占用端口,导致新连接无法绑定;
- 解决 :设置
SO_REUSEADDR选项(允许端口快速重用)。
4. TCP 可靠性机制(核心)
- 确认应答(ACK):接收方收到数据后返回确认;
- 超时重传:发送方超时未收到 ACK 则重传(超时时间动态计算);
- 滑动窗口:实现流量控制(接收方通过窗口大小告知发送方可发送的字节数);
- 拥塞控制:慢启动→拥塞避免→快重传→快恢复(避免网络拥塞);
- 序列号 / 确认号:解决乱序、重复问题;
- 校验和:检测数据传输中的错误。
三、C/C++ Socket 编程(代码实践必问)
1. 核心流程(服务端 + 客户端)
服务端(TCP 回显服务器示例):
cpp
运行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main() {
// 1. 创建socket(文件描述符)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置SO_REUSEADDR,解决TIME_WAIT占用端口
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定IP和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
server_addr.sin_port = htons(PORT); // 端口转换(主机序→网络序)
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
// 3. 监听连接(半连接队列大小设为5)
if (listen(listen_fd, 5) == -1) {
perror("listen failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受客户端连接(阻塞)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 5. 收发数据(回显)
char buf[BUF_SIZE];
ssize_t recv_len;
while ((recv_len = recv(conn_fd, buf, BUF_SIZE-1, 0)) > 0) {
buf[recv_len] = '\0';
printf("Received from client: %s\n", buf);
send(conn_fd, buf, recv_len, 0); // 回显数据
}
// 6. 关闭连接
close(conn_fd);
close(listen_fd);
return 0;
}
客户端示例:
cpp
运行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUF_SIZE 1024
int main() {
// 1. 创建socket
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 连接服务端
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect failed");
close(client_fd);
exit(EXIT_FAILURE);
}
// 3. 发送数据
char buf[BUF_SIZE];
printf("Enter message to send: ");
fgets(buf, BUF_SIZE, stdin);
send(client_fd, buf, strlen(buf)-1, 0); // 去掉fgets的换行符
// 4. 接收回显数据
ssize_t recv_len = recv(client_fd, buf, BUF_SIZE-1, 0);
if (recv_len > 0) {
buf[recv_len] = '\0';
printf("Received from server: %s\n", buf);
}
// 5. 关闭连接
close(client_fd);
return 0;
}
2. 核心函数 / 知识点解析
(1)字节序转换(必懂)
- 主机序:多数 CPU 是小端序(低字节存低地址);
- 网络序:强制大端序;
- 核心函数:
htons():主机序 → 网络序(短整型,如端口);htonl():主机序 → 网络序(长整型,如 IP);ntohs()/ntohl():反向转换。
(2)地址转换
- 旧接口:
inet_addr("127.0.0.1")(字符串→IP)、inet_ntoa(in_addr)(IP→字符串); - 新接口(推荐):
inet_pton()/inet_ntop()(支持 IPv6,更安全)。
(3)IO 多路复用(高性能核心,面试重中之重)
| 模型 | 原理 | 缺点 | 优点 |
|---|---|---|---|
| select | 监听 fd_set 集合,轮询检查 | 上限 FD_SETSIZE(默认 1024)、需重置集合 | 跨平台 |
| poll | 监听 pollfd 数组,轮询检查 | 轮询效率低 | 无 fd 数量上限 |
| epoll | 红黑树 + 就绪链表,事件驱动 | 仅 Linux 支持 | 无 fd 上限、无需轮询、高性能 |
epoll 核心考点:
- 水平触发(LT,默认):只要 fd 有数据 / 可写,就持续通知;
- 边缘触发(ET):仅当 fd 状态变化时通知(需配合非阻塞 socket,一次性读完数据);
- 常用 API:
epoll_create()、epoll_ctl()(添加 / 修改 / 删除事件)、epoll_wait()(等待事件)。
3. 常见编程问题(面试常考)
(1)TCP 粘包 / 拆包(高频编程题)
- 原因:TCP 是面向字节流,无数据边界,底层会根据 MTU 拆分 / 合并数据;
- 解决方案 (代码级必实现):
- 固定长度:每次发送固定字节数,接收方按固定长度读取;
- 分隔符:如
\n,接收方按分隔符分割数据; - 消息头 + 消息体:头中包含数据长度,接收方先读长度,再读对应字节数(最常用)。
(2)Socket 超时设置
-
场景:
connect()/recv()/send()默认阻塞,需设置超时; -
方法:
setsockopt()设置SO_RCVTIMEO/SO_SNDTIMEO:cpp
运行
struct timeval timeout; timeout.tv_sec = 5; // 5秒超时 timeout.tv_usec = 0; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
(3)并发处理方式
- 多进程:
fork(),父子进程共享 fd(需关闭多余 fd),处理僵尸进程(waitpid()); - 多线程:
pthread_create(),注意线程安全(如 fd 操作加锁); - IO 多路复用:
epoll(高性能,单进程处理上万连接)。
四、高频面试题(问答 + 编程)
- 解释 TCP 三次握手 / 四次挥手的过程和意义;
- TIME_WAIT 的作用和解决过多的方法;
- 实现 TCP 回显服务器(要求处理粘包);
- select/poll/epoll 的区别,epoll LT/ET 的区别;
- TCP 粘包的原因和解决方案;
- 如何设置 Socket 的超时时间;
- SO_REUSEADDR 的作用;
- TCP 拥塞控制的流程(慢启动、拥塞避免);
- 为什么 UDP 适合直播 / 游戏,TCP 适合文件传输;
- 用 epoll 实现一个简单的高并发 TCP 服务器。
总结
- 核心基础:掌握 TCP/UDP 区别、TCP 三次握手 / 四次挥手、TIME_WAIT、可靠性机制;
- 编程实践:熟练掌握 C/C++ Socket 核心流程(socket/bind/listen/accept/connect)、字节序转换、IO 多路复用(尤其是 epoll);
- 高频问题:TCP 粘包处理、Socket 超时设置、SO_REUSEADDR、epoll LT/ET 是面试中最常考察的代码级考点。