腾讯 IEG 后台开发一二面:IO 多路复用、粘包拆包与 CLOSE_WAIT
- 轮次
- 一面+二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面
- 实习相关(项目做了什么、你的角色)。
- IO 多路复用有哪些?讲一下它们的区别。
- TCP 与 UDP 的区别。
- Go 语言的 map 是并发安全的吗?
- 手撕代码:字符串加法。
- 目前有哪些 offer?
- 反问:面试评价(面试官说不能透露,但告知了人才画像)、业务、什么时候出结果?
二面
- 现在有几个 offer 了?
- 拷打项目。
- 项目中的 TCP 粘包、拆包是怎么解决的?
- 性能测试用的是短连接吗?为什么不会被 CLOSE_WAIT 耗尽系统资源?
- C++ 多重继承的指针类型转换:C 类继承 A 和 B,现有
C* ptr = new C(),如果把它向上转型成 A 指针或 B 指针,指向的地址会发生变化吗? - 在字节实习时使用什么 AI?
- 使用 AI 辅助编程时遇到过什么问题,如何缓解或解决?
《参考解析》
IO 多路复用的三条主线:select、poll、epoll。 select 用位图传 fd 集合,有 FD_SETSIZE(通常 1024)上限,每次调用都要把集合从用户态拷到内核、返回后还要遍历全部 fd 找就绪的,O(n);poll 用 pollfd 数组去掉了数量上限,但仍然每次全量拷贝 + 全量遍历;epoll 用内核红黑树管理注册的 fd(epoll_ctl 一次注册、长期有效),就绪事件挂到链表上,epoll_wait 直接返回就绪列表,O(1),只在注册时拷贝一次。epoll 还有两种触发模式:水平触发(LT,只要缓冲区有数据就一直通知,编程简单)与边缘触发(ET,状态变化才通知一次,必须循环读到 EAGAIN,否则数据会滞留)。再往上一层可以提 io_uring 与 Reactor/Proactor 模型,说明你知道 evolutions。
TCP 与 UDP 的对比要覆盖连接、可靠性、有序、边界、拥塞与开销。 TCP 面向连接(三次握手/四次挥手)、可靠(确认、重传、序号)、有序、面向字节流(没有消息边界,所以有粘包问题)、有流量控制(滑动窗口)与拥塞控制(慢启动、拥塞避免、快重传、快恢复),头部 20 字节起,适合文件、HTTP、数据库连接。UDP 无连接、不可靠、不保证顺序、面向报文(保留消息边界)、没有拥塞控制、头部 8 字节,延迟低,适合实时音视频、DNS、游戏同步、QUIC 的底层。要提一句「需要可靠性也可以在应用层自己做」(如 QUIC 在 UDP 上实现可靠传输)。
Go 的 map 不是并发安全的。 多个 goroutine 同时读写会触发运行时检测并直接抛 fatal error: concurrent map read and map write(这是 kill 进程级别的错误,recover 抓不到)。解法:读多写少用 sync.RWMutex 保护;或者用 sync.Map(适合「一次写入多次读取」或 key 集合稳定的场景,它内部用 read/dirty 两份结构减少锁竞争);或者分片加锁(把 key 哈希到 N 个 map 上,各自加锁);或者干脆改成 channel 串行化访问。要能说出「sync.Map 不是万能替代品,一般场景下 RWMutex + map 更快」。
字符串加法的手撕要点是「从后往前 + 进位 + 边界」。 双指针分别指向两个串末尾,逐位取数字(越界取 0)相加再加进位,结果用 StringBuilder 或 []byte 反序拼装,循环条件要包含 carry != 0,最后反转。要处理的边界:空串、前导零、长度差很大、结果进位多一位(999 + 1)。如果输入可能带符号或小数,先问清范围再写,不要默认为正数。
粘包拆包的本质是「TCP 是字节流,没有消息边界」。 三种主流解法:① 固定长度(简单,浪费带宽、不灵活);② 分隔符(如 \r\n,要处理转义,长度不可控时容易踩坑);③ 长度字段 + 消息体(最常用:先读固定长度的头部拿到 body 长度,再读满 body;头部本身可以用魔数 + 版本 + 长度 + 校验)。工程上还要处理:读缓冲的增量解析(半包留在缓冲里等下一次数据)、最大消息长度限制(防止恶意超长包打爆内存)、以及超时与心跳。Go 里就是自己维护一个 bufio.Reader 加状态机,Netty 里对应 LengthFieldBasedFrameDecoder。
短连接与 CLOSE_WAIT:CLOSE_WAIT 堆积是「应用没有 close」,不是「连接太多」。 四次挥手时,被动关闭方收到对端 FIN 后内核回 ACK,连接进入 CLOSE_WAIT,此时等应用调用 close() 才会发出自己的 FIN 转入 LAST_ACK。所以大量 CLOSE_WAIT 说明应用层漏了关闭(异常分支没关、连接池没回收、阻塞读没退出)。真正的风险是 fd 耗尽,不是「短连接本身」。至于「为什么没被耗尽」,可能的真实原因是:客户端用连接池复用短连接、请求量没到 fd 上限、或者用了 SO_LINGER/HTTP keep-alive 让连接很快走完挥手(TIME_WAIT 出现在主动关闭方,与 CLOSE_WAIT 是两回事)。答题时把「谁主动关、谁进哪个状态」讲清,比背结论更重要。
C++ 多重继承的向上转型:指针地址可能变化,这是 this 指针调整。 class C : public A, public B 时,对象内存布局通常按继承顺序排列:A 的子对象在前、B 的子对象在后,然后是 C 自己的成员。C* 转 B* 时编译器会把地址加上 B 子对象的偏移量(这是静态转换,编译期算好),所以指针值会变;转 A* 通常是偏移 0,值不变。static_cast 与隐式转换都会做这个调整;reinterpret_cast 不会,所以拿 reinterpret_cast<B*>(c) 去用是未定义行为。虚继承更复杂:虚基类子对象的位置不固定,需要靠 vtable 里的虚基类偏移在运行期计算,转换会有一次间接寻址。这个问题的价值在于它能区分「知道指针就是个地址」和「知道对象布局」。
「实习用什么 AI、遇到过什么问题」要答成方法论。 工具可以提(Claude Code、Cursor、Copilot 之类),但重点在三件事:它在你的流程里承担什么(读陌生代码、写测试与样板、做第一轮 review、排障时归纳日志)、你怎么验证(编译 + 单测 + lint + 小步提交 + 人工逐行看核心逻辑)、以及真实踩过的问题(生成过时 API、编造不存在的库、看似正确但漏掉边界条件、顺手改了无关文件导致 diff 膨胀)。缓解手段对应着来:给足上下文与约定、要求先输出改动计划再动手、限制可改文件范围、禁止运行格式化与批量重命名、用 git diff --stat 检查波及面。
「有几个 offer」这类问题不是打探隐私,是在评估你的决策窗口。 如实说数量级即可,重点表达「我更看重这个岗位的什么」,不要用来抬价也不要编造。反问环节里,请面试官讲「人才画像」是很有价值的一问——它告诉你这个团队真正在筛什么,比问薪资有用得多。