这篇文章聚焦网络编程和进程间通信(IPC)的面试题,特别补充了 Windows RPC 的底层原理(不再只是”照着用”),以及 Linux 和 Windows 在这些领域的对比。
第一部分:TCP/UDP 基础
Q1:TCP 和 UDP 的核心区别?
记忆点:TCP 可靠有序有连接,UDP 不可靠无序无连接。TCP 牺牲速度换可靠,UDP 牺牲可靠换速度。
1
2
3
4
5
6
7
8
9
| TCP UDP
连接 需要三次握手建立连接 无连接,直接发
可靠性 可靠(重传、确认、排序) 不可靠(可能丢包、乱序)
顺序 保证有序到达 不保证顺序
流量控制 有(滑动窗口) 无
拥塞控制 有(慢启动、拥塞避免) 无
传输单位 字节流(无边界) 数据报(有边界)
头部大小 20 字节起 8 字节
适用场景 HTTP、FTP、数据库 DNS、视频直播、游戏
|
Q2:TCP 三次握手的过程和原因?
记忆点:SYN → SYN+ACK → ACK。三次是为了双方都确认自己的收发能力正常,两次不够(服务端不知道客户端能否收到自己的回复)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 客户端 服务端
│ ── SYN (seq=x) ──────────> │ 第 1 次:客户端说"我要连接"
│ │ 客户端确认:自己能发
│ <── SYN+ACK (seq=y,ack=x+1) │ 第 2 次:服务端说"收到,我也准备好了"
│ │ 服务端确认:自己能收、能发
│ ── ACK (ack=y+1) ─────────> │ 第 3 次:客户端说"收到你的确认"
│ │ 客户端确认:对方能收
│ 连接建立 │ 双方都确认:收发正常
为什么不能两次?
如果只有两次,服务端发了 SYN+ACK 就认为连接建立
但客户端可能根本没收到 → 服务端白白分配资源
经典场景:过期的 SYN 到达 → 服务端建立了一个"幽灵连接"
|
Q3:TCP 四次挥手为什么是四次?
记忆点:因为 TCP 是全双工的,每个方向的关闭需要独立的 FIN+ACK,所以是 2×2=4 次。中间有个 CLOSE_WAIT/TIME_WAIT。
1
2
3
4
5
6
7
8
9
10
11
| 客户端 服务端
│ ── FIN ─────────────────> │ 第 1 次:客户端说"我发完了"
│ <── ACK ───────────────── │ 第 2 次:服务端说"收到,但我可能还有数据要发"
│ │
│ (服务端继续发数据) │ ← 半关闭状态
│ │
│ <── FIN ───────────────── │ 第 3 次:服务端说"我也发完了"
│ ── ACK ─────────────────> │ 第 4 次:客户端说"收到"
│ │
│ TIME_WAIT (等 2MSL) │ ← 等待可能重传的 FIN
|
Q4:TIME_WAIT 状态有什么用?为什么要等 2MSL?
记忆点:防止最后一个 ACK 丢失导致对方重发 FIN 时找不到连接;防止旧连接的延迟数据包被新连接误收。2MSL 是一个报文在网络中最大存活时间的两倍。
1
2
3
4
5
6
7
8
9
| 面试追问:TIME_WAIT 过多怎么办?
├── 现象:大量短连接的服务器上,端口被 TIME_WAIT 占满
├── 查看:netstat -an | grep TIME_WAIT | wc -l
├── 解决:
│ ├── 开启 SO_REUSEADDR(允许重用处于 TIME_WAIT 的端口)
│ ├── 调整内核参数 tcp_tw_reuse / tcp_tw_recycle
│ ├── 使用长连接(HTTP Keep-Alive)减少连接频率
│ └── 让客户端主动关闭(TIME_WAIT 出现在主动关闭的一方)
|
Q5:TCP 粘包/拆包是怎么回事?
记忆点:TCP 是字节流协议没有消息边界,多个 send 的数据可能被合并接收(粘包)或一个 send 的数据被拆成多次接收(拆包)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 发送端:
send("Hello")
send("World")
接收端可能收到:
情况 1(正常): "Hello" + "World"
情况 2(粘包): "HelloWorld"
情况 3(拆包): "Hel" + "loWorld"
情况 4(混合): "HelloWor" + "ld"
解决方案:
├── 固定长度:每个消息固定 N 字节
├── 分隔符:用 \r\n 或特殊字符分隔消息
├── 长度前缀:消息头部写明消息体长度(最常用)
│ [4字节长度][消息体...]
└── 应用层协议:HTTP 的 Content-Length / chunked
注意:UDP 没有粘包问题(每个数据报是独立的)
|
Q6:TCP 的滑动窗口和拥塞控制?
记忆点:滑动窗口做流量控制(接收方告诉发送方”我还能收多少”),拥塞控制做网络保护(避免往网络里灌太多数据)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 滑动窗口(Flow Control):
接收方通过 ACK 中的 window size 告诉发送方:
"我的缓冲区还剩 X 字节,你别发太多"
发送方 接收方
──数据──> 缓冲区 [██████░░░░] window=4
──数据──> 缓冲区 [████████░░] window=2
──数据──> 缓冲区 [██████████] window=0 ← 停!
(等待...) 应用读取数据
缓冲区 [████░░░░░░] window=6 ← 可以继续发了
拥塞控制(Congestion Control)四个阶段:
1. 慢启动(Slow Start):指数增长,cwnd 每 RTT 翻倍
2. 拥塞避免(Congestion Avoidance):到阈值后线性增长
3. 快重传(Fast Retransmit):收到 3 个重复 ACK → 立即重传
4. 快恢复(Fast Recovery):不从 1 开始,从阈值/2 开始
|
第二部分:Socket 编程
Q7:Socket 编程的基本流程?
记忆点:服务端 socket→bind→listen→accept→read/write→close;客户端 socket→connect→read/write→close。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 服务端 客户端
socket() 创建套接字 socket() 创建套接字
│ │
bind() 绑定地址和端口 │
│ │
listen() 开始监听 │
│ │
accept() ← 阻塞等待连接 ──────────── connect() 发起连接
│ 返回新的 socket fd │
│ │
read() <────────────────────────── write() 发送数据
write() ──────────────────────────> read() 接收数据
│ │
close() 关闭连接 close() 关闭连接
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Linux 代码骨架(服务端)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8080);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
int client_fd = accept(server_fd, NULL, NULL);
char buf[1024];
int n = read(client_fd, buf, sizeof(buf));
write(client_fd, "OK", 2);
close(client_fd);
close(server_fd);
|
Q8:Windows 和 Linux 的 Socket API 差异?
记忆点:核心 API 几乎一致(BSD Socket 标准),但 Windows 要先 WSAStartup 初始化,用 closesocket 而非 close,错误码用 WSAGetLastError。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| Linux Windows
初始化 不需要 WSAStartup(MAKEWORD(2,2), &wsaData)
头文件 <sys/socket.h> <winsock2.h> + <ws2tcpip.h>
<netinet/in.h>
<unistd.h>
关闭 socket close(fd) closesocket(sock)
错误码 errno WSAGetLastError()
描述符类型 int (fd) SOCKET (typedef UINT_PTR)
IO 模型 epoll IOCP
非阻塞设置 fcntl(fd, F_SETFL) ioctlsocket(sock, FIONBIO)
清理 不需要 WSACleanup()
链接库 不需要 ws2_32.lib
// Windows 代码额外步骤
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData); // 必须先初始化
// ... socket 操作 ...
WSACleanup(); // 程序结束时清理
|
第三部分:IO 模型
Q9:五种 IO 模型是什么?
记忆点:阻塞、非阻塞、IO 多路复用、信号驱动、异步 IO。前四种本质都是同步(需要自己读数据),只有 AIO 是真异步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ① 阻塞 IO(Blocking IO)
调用 read() → 如果没数据就一直等 → 有数据了才返回
类比:你在餐厅等位,站在门口干等
② 非阻塞 IO(Non-blocking IO)
调用 read() → 没数据立即返回 EAGAIN → 你自己轮询
类比:你每隔 5 分钟去问一次"有位子了吗"
③ IO 多路复用(select/poll/epoll)
用一个系统调用监视多个 fd → 哪个就绪通知你 → 再去读
类比:服务员帮你盯着所有桌子,哪桌空了告诉你
④ 信号驱动 IO(Signal-driven IO)
注册 SIGIO 信号 → 数据就绪时内核发信号给你 → 你再去读
类比:留个电话号码,有位子了餐厅打电话通知你
⑤ 异步 IO(Asynchronous IO)
发起 aio_read() → 内核完成所有工作(等待+复制)→ 通知你"已经读好放在缓冲区了"
类比:外卖送到你手里,你啥都不用干
|
Q10:select、poll、epoll 的区别?
记忆点:select 有 1024 fd 限制且每次全量拷贝;poll 去掉了限制但还是线性扫描;epoll 用事件驱动只通知就绪的 fd,O(1) 效率。
1
2
3
4
5
6
7
8
9
| select poll epoll
───────────────────────────────────────────────────
fd 数量限制 1024(FD_SETSIZE) 无限制 无限制
数据结构 fd_set 位图 pollfd 数组 红黑树 + 就绪链表
每次调用 全量拷贝 fd 集 全量拷贝 fd 集 不需要重复拷贝
返回后 遍历所有 fd 遍历所有 fd 只返回就绪的 fd
时间复杂度 O(n) O(n) O(1) 事件通知
触发模式 水平触发(LT) 水平触发(LT) LT + 边缘触发(ET)
跨平台 ✅ 全平台 ✅ Unix 系 ❌ 仅 Linux
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // epoll 使用示例
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = server_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (true) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_fd) {
// 新连接
} else {
// 数据可读
}
}
}
|
Q11:epoll 的 LT(水平触发)和 ET(边缘触发)的区别?
记忆点:LT 只要 fd 有数据就一直通知(数据没读完下次还通知);ET 只在状态变化时通知一次(数据没读完不再通知,必须一次读完)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| LT(水平触发,默认):
数据到达 → 通知你 → 你只读了一部分 → 下次 epoll_wait 还会通知你
特点:编程简单,不会丢数据
类比:闹钟响了你没起床,它会一直响
ET(边缘触发):
数据到达 → 通知你一次 → 你必须一次读完(循环读直到 EAGAIN)
特点:效率更高(减少通知次数),但编程复杂,必须用非阻塞 IO
类比:闹钟只响一次,没起床就没了
ET 的必须搭配:
├── 非阻塞 fd(否则最后一次 read 会卡住)
└── 循环读直到 EAGAIN
while (true) {
n = read(fd, buf, sizeof(buf));
if (n == -1 && errno == EAGAIN) break; // 读完了
process(buf, n);
}
|
Q12:Windows 的 IOCP 是什么?和 epoll 对比?
记忆点:IOCP 是 Windows 的异步 IO 完成端口,是真正的异步(内核完成数据拷贝后通知你)。epoll 是同步多路复用(通知你数据就绪,你自己读)。IOCP 是 Proactor 模式,epoll 是 Reactor 模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| epoll(Reactor 模式):
1. epoll_wait 通知你:fd 上有数据可读了
2. 你自己调用 read() 把数据从内核复制到用户空间
3. 你处理数据
← 通知就绪 + 自己读
IOCP(Proactor 模式):
1. 你先提交一个异步读请求(WSARecv),带上缓冲区
2. 内核在后台完成读操作,数据直接放到你的缓冲区
3. GetQueuedCompletionStatus 通知你:数据已经读好了
← 内核全包了,通知完成
对比:
epoll IOCP
模式 Reactor Proactor
数据拷贝 应用层自己做 内核帮你做好
真异步 否(IO 多路复用) 是(真正异步 IO)
线程模型 通常单线程事件循环 天然多线程(线程池)
编程复杂度 相对简单 较复杂(异步思维)
平台 Linux Windows
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // IOCP 基本流程
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 将 socket 关联到 IOCP
CreateIoCompletionPort((HANDLE)clientSocket, iocp, (ULONG_PTR)context, 0);
// 发起异步读(不会阻塞)
WSARecv(clientSocket, &dataBuf, 1, &bytesRecv, &flags, &overlapped, NULL);
// 工作线程等待完成通知
while (true) {
GetQueuedCompletionStatus(iocp, &bytesTransferred, &key, &overlapped, INFINITE);
// 到这里时,数据已经在 dataBuf 里了!
processData(dataBuf, bytesTransferred);
}
|
第四部分:网络编程模型
Q13:Reactor 和 Proactor 模式的区别?
记忆点:Reactor 是”通知你事件就绪,你自己处理”;Proactor 是”帮你处理好了,通知你已完成”。Reactor = 我告诉你水烧开了你自己倒;Proactor = 我帮你倒好了告诉你可以喝了。
1
2
3
4
5
6
7
| Reactor(Linux 主流:epoll):
注册事件 → 等待就绪 → 回调处理(自己读写)
代表:libevent、libev、Muduo、Redis、Nginx
Proactor(Windows 主流:IOCP):
发起异步操作 → 等待完成 → 回调处理(数据已就绪)
代表:Boost.Asio(跨平台封装,Linux 上模拟 Proactor)
|
Q14:常见的服务器并发模型?
记忆点:从简单到复杂 —— 单线程阻塞 → 多进程 → 多线程 → 线程池 → IO多路复用 → Reactor → 主从 Reactor。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| ① 每连接一进程(fork 模型)
accept → fork → 子进程处理
简单但开销大,进程创建/切换成本高
② 每连接一线程
accept → 创建线程 → 线程处理
比进程轻量但线程数有上限(万级连接就撑不住)
③ 线程池
预先创建 N 个线程 → accept 后把连接分配给空闲线程
避免频繁创建/销毁线程,但仍受线程数限制
④ 单 Reactor 单线程(Redis 模型)
一个线程 epoll 处理所有连接
简单高效但无法利用多核
⑤ 单 Reactor 多线程
主线程 epoll 接收事件 → 分发给工作线程池处理
利用多核但 Reactor 本身可能成瓶颈
⑥ 主从 Reactor(Nginx/Netty 模型)
主 Reactor 只负责 accept → 分配给从 Reactor
每个从 Reactor 在自己的线程中处理 IO
目前最主流的高性能方案
⑥ 的结构图:
主线程 (Main Reactor)
│ accept 新连接
├──────────> 从 Reactor 1 (线程1):处理连接 1,2,3
├──────────> 从 Reactor 2 (线程2):处理连接 4,5,6
└──────────> 从 Reactor 3 (线程3):处理连接 7,8,9
|
第五部分:进程间通信(IPC)
Q15:Linux 有哪些 IPC 方式?各自特点?
记忆点:管道(父子进程)、命名管道/FIFO(任意进程)、消息队列(带类型的消息)、共享内存(最快)、信号量(同步)、Socket(跨网络)、信号(异步通知)。
1
2
3
4
5
6
7
8
9
10
| IPC 方式 速度 方向 关系要求 适用场景
管道 (pipe) 中 单向 父子进程 简单数据流 (shell 管道)
命名管道(FIFO) 中 单向/双向 任意进程 无亲缘关系的进程通信
消息队列 中 双向 任意进程 结构化消息传递
共享内存 最快 双向 任意进程 大量数据交换(需自行同步)
信号量 N/A N/A 任意进程 同步/互斥(不传数据)
信号(signal) 快 单向 任意进程 异步事件通知(SIGTERM 等)
Unix Socket 中 双向 同一台机器 类似网络通信但更快
TCP/UDP Socket 慢 双向 跨网络 跨机器通信
|
Q16:管道(pipe)的原理?
记忆点:管道是内核中的一块缓冲区(默认 64KB),写端写入、读端读出,单向半双工。匿名管道只能用于有亲缘关系的进程(fork 继承 fd),命名管道(FIFO)可用于任意进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 匿名管道
int pipefd[2];
pipe(pipefd); // pipefd[0] 读端, pipefd[1] 写端
if (fork() == 0) {
// 子进程:写
close(pipefd[0]);
write(pipefd[1], "hello", 5);
close(pipefd[1]);
} else {
// 父进程:读
close(pipefd[1]);
char buf[10];
read(pipefd[0], buf, sizeof(buf));
close(pipefd[0]);
}
// 命名管道
mkfifo("/tmp/myfifo", 0666);
// 进程 A: open("/tmp/myfifo", O_WRONLY) → write
// 进程 B: open("/tmp/myfifo", O_RDONLY) → read
|
Q17:共享内存为什么最快?怎么用?
记忆点:共享内存直接让两个进程映射同一块物理内存,不需要内核中转(零拷贝),所以最快。但需要自行用信号量或互斥锁做同步。
1
2
3
4
5
6
7
8
9
| 其他 IPC 的数据流:
进程A → 用户空间拷贝到内核 → 内核拷贝到进程B 用户空间
两次拷贝!
共享内存的数据流:
进程A ───┐
├── 直接读写同一块物理内存
进程B ───┘
零拷贝!
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // POSIX 共享内存(Linux)
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void* ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 现在 ptr 指向的内存两个进程都能访问
sprintf((char*)ptr, "Hello from process A");
// 进程 B 同样 shm_open + mmap 后就能读到数据
// Windows 共享内存
HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, 4096, "MySharedMem");
void* ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 4096);
|
Q18:消息队列(Message Queue)的特点?
记忆点:消息队列在内核中维护一个消息链表,支持按消息类型读取(可以选择性接收),自带同步(不需要额外加锁),消息有边界不会粘包。
1
2
3
4
5
6
7
8
| // POSIX 消息队列
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);
mq_send(mq, "hello", 5, priority);
mq_receive(mq, buf, sizeof(buf), &priority);
// 对比管道 vs 消息队列:
// 管道:字节流,无边界,FIFO
// 消息队列:消息为单位,有边界,可按优先级/类型
|
Q19:Windows 有哪些 IPC 方式?
记忆点:除了和 Linux 共有的 Socket/管道/共享内存,Windows 还有特有的 COM、WM_COPYDATA、Mailslot、Named Pipe(双向)和 RPC。
1
2
3
4
5
6
7
8
9
10
11
12
13
| Windows IPC 方式 特点
匿名管道 (Anonymous Pipe) 类似 Linux pipe,父子进程间
命名管道 (Named Pipe) 双向通信!不同于 Linux FIFO
支持网络透明(\\server\pipe\name)
Windows 上最常用的本地 IPC
共享内存 (File Mapping) CreateFileMapping + MapViewOfFile
WM_COPYDATA 通过窗口消息传递数据(仅 GUI 程序)
Mailslot 单向广播式通信
COM/DCOM 组件对象模型(进程间方法调用)
Windows RPC 远程过程调用框架
Socket 和 Linux 一致
剪贴板 (Clipboard) 最简单但最受限
|
1
2
3
4
5
6
7
8
| Windows Named Pipe vs Linux FIFO:
Windows Named Pipe Linux FIFO
方向 双向(全双工) 单向(半双工)
网络透明 ✅ 可跨机器 ❌ 仅本机
消息模式 支持消息/字节流 仅字节流
安全性 支持 ACL 权限控制 文件权限
异步 支持 Overlapped IO 需要非阻塞 IO
|
第六部分:RPC 深入理解
Q20:RPC 到底是什么?
记忆点:RPC(远程过程调用)让你像调用本地函数一样调用远程机器上的函数。底层封装了序列化、网络传输、反序列化的全部细节。
1
2
3
4
5
6
7
8
9
10
11
12
| 本地函数调用:
result = add(1, 2); // 直接调用,同一进程
RPC 调用(看起来一样):
result = add(1, 2); // 实际上:
// 1. 把参数 (1,2) 序列化成字节流
// 2. 通过网络发送到远程服务器
// 3. 远程服务器反序列化,执行 add(1,2)
// 4. 把结果 3 序列化
// 5. 通过网络发回
// 6. 本地反序列化得到结果 3
// 你感觉不到这些细节
|
Q21:RPC 的完整调用流程?
记忆点:Client Stub(客户端桩)序列化参数 → 网络传输 → Server Stub(服务端桩)反序列化 → 执行 → 原路返回结果。Stub 就是”代理人”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 客户端 服务端
应用代码 实际函数
result = add(1, 2) int add(int a, int b)
│ ▲
▼ │
Client Stub(客户端代理) Server Stub(服务端代理)
├── 把函数名、参数序列化 ├── 反序列化参数
│ → {func:"add", args:[1,2]} │ → a=1, b=2
├── 封装成网络消息 ├── 调用真正的 add(1,2)
│ ├── 序列化返回值 → {result:3}
│ │
└──── 网络传输(TCP/HTTP/...)────────┘
"Stub" 这个词的意思 = 存根/桩
它是一个代理,帮你处理序列化和网络通信的全部细节
|
Q22:Windows RPC 的架构?(你之前用过的那个)
记忆点:Windows RPC 用 IDL 文件定义接口 → MIDL 编译器生成 Stub 代码 → 客户端和服务端各拿一半 Stub → 运行时通过 RPC Runtime 通信。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| 开发流程:
Step 1: 写 IDL 文件(接口定义语言)
┌─────────────────────────────────────┐
│ // calculator.idl │
│ [uuid(12345678-...)] │
│ interface ICalculator { │
│ int Add([in] int a, [in] int b);│
│ int Multiply([in] int a, │
│ [in] int b); │
│ } │
└─────────────────────────────────────┘
Step 2: MIDL 编译器处理 IDL
midl calculator.idl
生成:
├── calculator_h.h → 公共头文件(接口定义)
├── calculator_c.c → Client Stub(客户端代理代码)
├── calculator_s.c → Server Stub(服务端代理代码)
└── dlldata.c → 辅助代码
Step 3: 客户端代码
// 绑定到服务端
RpcStringBindingCompose(NULL, "ncacn_np", "\\\\server",
"\\pipe\\calculator", NULL, &binding);
RpcBindingFromStringBinding(binding, &hBinding);
// 调用(看起来就像本地函数!)
int result = Add(hBinding, 1, 2); // 实际通过网络调用
Step 4: 服务端代码
// 注册接口
RpcServerRegisterIf(ICalculator_v1_0_s_ifspec, NULL, NULL);
// 监听
RpcServerUseProtseqEp("ncacn_np", max_calls, "\\pipe\\calculator", NULL);
RpcServerListen(1, max_calls, FALSE);
// 实现真正的函数
int Add(handle_t h, int a, int b) { return a + b; }
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| Windows RPC 分层架构:
你的代码(调用 Add(1,2))
│
Client Stub(MIDL 自动生成)
├── 序列化参数(NDR 格式)
│
RPC Runtime(rpcrt4.dll)
├── 连接管理
├── 安全认证
├── 重试/超时
│
传输协议
├── ncacn_np → Named Pipe(最常用于本机/局域网)
├── ncacn_ip_tcp → TCP
├── ncalrpc → 本机 LPC(最快,同一台机器内)
└── ncacn_http → HTTP(穿越防火墙)
│
网络
│
Server Stub → 反序列化 → 调用真正的函数
|
Q23:为什么说你之前”只会照着用”?哪些细节容易被追问?
记忆点:IDL 语法细节、NDR 序列化格式、绑定方式(字符串绑定 vs 端点映射)、安全认证(NTLM/Kerberos)、内存管理(MIDL_user_allocate)、异步 RPC。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| 面试常见追问及回答要点:
Q: IDL 中 [in]、[out]、[in,out] 是什么意思?
A: 数据方向标记
[in] = 客户端 → 服务端(参数传入)
[out] = 服务端 → 客户端(结果传出)
[in,out] = 双向(传入并可能被修改后返回)
这决定了 Stub 代码序列化哪些数据
Q: RPC 的内存谁负责释放?
A: 遵循"谁分配谁释放"原则
[in] 参数:客户端分配和释放
[out] 参数:服务端分配(用 MIDL_user_allocate),客户端释放(用 MIDL_user_free)
这两个函数你必须实现!
Q: 绑定句柄(Binding Handle)有哪几种?
A: 三种
├── 自动绑定(auto_handle):最简单,MIDL 自动处理
├── 隐式绑定(implicit_handle):全局绑定句柄
└── 显式绑定(explicit_handle):每个调用可以用不同的服务端
显式最灵活但最啰嗦
Q: NDR(Network Data Representation)是什么?
A: Windows RPC 的序列化格式
处理不同架构的字节序、对齐、类型大小差异
类似 Google 的 protobuf,但更老也更底层
Q: RPC 的安全认证?
A: RPC Runtime 支持多种安全提供者:
├── NTLM(传统 Windows 认证)
├── Kerberos(域环境推荐)
├── SCHANNEL(SSL/TLS)
└── 可以设置认证级别:
RPC_C_AUTHN_LEVEL_NONE → 不认证
RPC_C_AUTHN_LEVEL_CONNECT → 连接时认证
RPC_C_AUTHN_LEVEL_PKT_INTEGRITY → 每包完整性校验
RPC_C_AUTHN_LEVEL_PKT_PRIVACY → 每包加密
|
Q24:现代 RPC 框架对比?
记忆点:Windows RPC 是微软专有的。现代跨平台 RPC 首选 gRPC(Google,用 protobuf,HTTP/2)。
1
2
3
4
5
6
7
8
9
| Windows RPC gRPC Thrift
公司 Microsoft Google Facebook(Meta)
IDL MIDL (.idl) Protobuf(.proto) Thrift (.thrift)
序列化 NDR Protobuf Thrift Binary
传输 Named Pipe/TCP HTTP/2 TCP
跨语言 主要 C/C++ 多语言 多语言
跨平台 仅 Windows 全平台 全平台
流式传输 不支持 支持(双向流) 不支持
现状 遗留系统 主流首选 仍在使用
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // gRPC 的 IDL 长这样(.proto 文件)
syntax = "proto3";
service Calculator {
rpc Add (AddRequest) returns (AddResponse);
rpc ServerStream (Request) returns (stream Response); // 服务端流
}
message AddRequest {
int32 a = 1;
int32 b = 2;
}
message AddResponse {
int32 result = 1;
}
|
第七部分:网络编程实战问题
Q25:如何处理大量并发连接?(C10K / C10M 问题)
记忆点:C10K 用 epoll/IOCP + 非阻塞 IO + 事件驱动已经解决;C10M 需要内核旁路(DPDK/XDP)、零拷贝、无锁数据结构。
1
2
3
4
5
6
7
8
9
10
11
| C10K(万级连接)解决方案:
├── IO 多路复用(epoll / IOCP)
├── 非阻塞 IO
├── 事件驱动架构(Reactor)
└── 连接池 + 线程池
C10M(千万级连接)额外需要:
├── 内核旁路(Bypass Kernel):DPDK 直接在用户态处理网络包
├── 零拷贝(Zero-copy):sendfile、mmap、splice
├── 无锁编程
└── CPU 亲和性绑定
|
Q26:什么是零拷贝(Zero-Copy)?
记忆点:传统文件发送需要 4 次拷贝(磁盘→内核→用户→内核→网卡),零拷贝通过 sendfile/mmap 减少到 2 次(磁盘→内核→网卡),数据不经过用户空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 传统发送文件:
磁盘 →① 内核缓冲区 →② 用户缓冲区 →③ Socket 缓冲区 →④ 网卡
4 次拷贝,2 次用户态/内核态切换
sendfile 零拷贝:
磁盘 →① 内核缓冲区 ──────────────→② 网卡
2 次拷贝,0 次用户态拷贝
数据全程不经过用户空间
// Linux
sendfile(socket_fd, file_fd, &offset, count);
// Windows(TransmitFile)
TransmitFile(socket, fileHandle, 0, 0, NULL, NULL, 0);
|
Q27:心跳机制怎么设计?
记忆点:定时发送小包探测连接是否存活。TCP 自带 Keep-Alive(默认 2 小时太长),应用层心跳更灵活。客户端定时发 Ping,服务端回 Pong,超时未收到 Pong 则断开重连。
1
2
3
4
5
6
7
8
9
10
| 应用层心跳设计:
客户端每 30 秒发送 PING
服务端收到后回复 PONG
客户端连续 3 次没收到 PONG → 判定连接断开 → 重连
优于 TCP Keep-Alive 的原因:
├── TCP Keep-Alive 默认 2 小时才检测
├── 某些 NAT/防火墙会静默丢弃 Keep-Alive 包
├── 应用层心跳可以携带业务数据(如负载信息)
└── 可以精细控制超时策略
|
速查表
1
2
3
4
5
6
7
8
9
10
11
12
| TCP 三握手 SYN → SYN+ACK → ACK(双方确认收发能力)
TCP 四挥手 FIN → ACK → FIN → ACK(全双工各自关闭)
TIME_WAIT 主动关闭方等 2MSL(防 ACK 丢失 + 防旧包串线)
粘包解决 长度前缀 / 分隔符 / 固定长度
IO 模型 阻塞 / 非阻塞 / 多路复用 / 信号驱动 / 异步
epoll vs IOCP Reactor(通知就绪) vs Proactor(通知完成)
LT vs ET 持续通知 vs 只通知一次
IPC 最快 共享内存(零拷贝,但需自行同步)
RPC 本质 序列化 → 网络传输 → 反序列化 → 调用 → 原路返回
Win RPC IDL → MIDL编译 → Stub → RPC Runtime → 传输协议
gRPC Protobuf + HTTP/2,现代 RPC 首选
零拷贝 sendfile / mmap,数据不经过用户空间
|
下篇将深入讲解 锁、并发编程与内存模型 的面试题。