腾讯 IEG 服务器开发一面(TCP与Go八股)
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- TCP 为什么要三次握手?为什么要四次挥手?
- 如果 TCP 连接的一端突然断电,另一端能否知晓、什么时候知晓?如果一端主动关机呢?
- protobuf 中修改 proto 文件里某个结构的某个变量名,是否会影响已序列化数据的反序列化?
- Golang 中 map 的底层是什么?
- 是否了解 KCP 协议?
- 手撕:给一个二进制流,把每个字节看作
uint8,统计出现最多的数字(只需讲思路)。 - 实习和开源项目深挖:面试官比着简历上的实习与项目逐个提问,重点问了开源项目。
《参考解析》
三次握手、四次挥手:三次握手的本质是「双方都要确认自己和对方的收发能力,并且同步初始序列号」。SYN(seq=x) → SYN+ACK(seq=y, ack=x+1) → ACK(ack=y+1)。两次不够:服务端发出 SYN+ACK 后并不知道客户端是否收到,且历史上延迟的重复 SYN 会让服务端建立无效连接;四次则浪费一个来回,因为第二、三步可以合并。四次挥手是因为 TCP 是全双工、两个方向要各自关闭:主动方发 FIN 表示「我没有数据要发了」,对方先回 ACK(此时它可能还有数据要发),等它数据发完再发自己的 FIN,主动方回 ACK 并进入 TIME_WAIT,等 2MSL 确保最后的 ACK 能到达、并让本次连接的残余报文在网络中消亡。
对端断电与主动关机:要区分三种情况。① 对端进程崩溃但主机还在:内核会回收连接并发出 RST(或对后续数据回 RST),本端很快收到 RST 报错(ECONNRESET)。② 对端主机断电/掉网(没有 FIN、没有 RST):本端感知不到,只能靠重传超时——发数据后按 RTO 指数退避重传,Linux 上 tcp_retries2 默认 15 次,大约 13~30 分钟后才判定连接失效;如果期间没有数据要发,就只能等 TCP keepalive(SO_KEEPALIVE,默认空闲 7200 秒后才开始探测)或应用层心跳;所以长连接服务一律自己做应用层心跳,并配读写超时。③ 对端主动关机:操作系统正常关闭流程会给所有连接发 FIN,本端立刻可感知(读返回 0,进入 CLOSE_WAIT),前提是应用及时读 socket;如果对端是 kill -9 关掉进程,则是 RST 路径。
protobuf 改字段名不影响反序列化:protobuf 的二进制 wire format 里只有字段编号(field number)+ wire type + 值,字段名只存在于 .proto 源码和生成代码里,不参与编码。所以只改名字(编号不变)时,旧数据照样能被新代码解析。真正破坏兼容的是改字段编号、改类型(尤其是 wire type 变化的,如 int32 与 string/bytes 之类)、把字段从 optional 语义上改成 repeated,以及删除字段后把编号复用给新字段——正确做法是给废弃编号加 reserved。所以线上规范是:编号一旦发布就冻结,改名随意但建议同步更新注释与 json_name。
Go map 的底层:hmap + 若干 bmap(bucket)。每个 bucket 固定放 8 个键值对,前面 8 个字节是 tophash(哈希高 8 位),用于快速比对;key 和 value 分别连续存放(不是 k-v-k-v),便于内存对齐。哈希值的低位选 bucket、高 8 位做 tophash。装不下就挂溢出桶(overflow bucket)。负载因子超过 6.5 触发增量扩容(容量翻倍,B++,迁移分次摊到后续的写操作里,靠 oldbuckets + nevacuate 渐进搬迁);如果是因为溢出桶太多(极端哈希冲突)则等量扩容。map 的遍历顺序是随机的(随机起始 bucket 与 cell)。两条工程结论:map 不是并发安全的,并发读写会直接 fatal error: concurrent map writes(用 sync.Map 或加锁);预估容量时用 make(map[K]V, n) 可以减少扩容。
KCP 协议:KCP 是建立在 UDP 之上的可靠传输协议,目标是「用带宽换延迟」,比 TCP 更快。核心做法:滑动窗口 + 重传,但RTO 计算更激进(不复用 TCP 的指数退避,RTO = RTT + 4 * RTTVar 并设下限 30ms),支持**选择性重传(ACK + una 机制)**而不是像 TCP 那样回退到丢失点;还有快速重传(跳过 2 个 ACK 即重传)、非退让流控、可配置的 nodelay/interval/resend/nc(如 ikcp_nodelay(kcp, 1, 10, 2, 1) 开启 nodelay、10ms 内部刷新、快速重传、关闭流控),以及可选的 FEC 前向纠错。代价是更费带宽、且需要自己在应用层处理拥塞(KCP 本身流控保守、公平性不如 TCP,公网大流量滥用会影响其他流)。典型场景是游戏实时对战、音视频这类「延迟敏感、能接受冗余」的链路。
手撕:统计二进制流中出现最多的字节:思路讲清两点就够。① 计数结构:取值范围是 0~255 且是整数,用长度 256 的 []uint32(或 [256]uint32 数组)比哈希表更快——数组下标即字节值,O(1) 且无哈希开销、无扩容,这是标准答案;哈希表也正确,但多了哈希与内存分配,只有在「取值范围极大」时才更合适。② 遍历:for _, b := range data { cnt[b]++ }(byte 在 Go 里就是 uint8),再线性扫一遍 256 个桶取最大值;要处理并列的情况(返回全部最大值或第一个,先问面试官)。复杂度 O(n),额外空间 O(1)(256 个计数器)。若流非常大,可以分块读取 + 累加同一组计数器,避免一次性读入内存。
面试复盘:原帖反馈整体体验很好、线下 60 分钟左右、自评答得不错,当晚就约了第二天下午的二面。这类「比着简历问」的面试,项目要能讲出自洽的三层:做了什么、为什么这么选、遇到什么问题怎么定位解决;八股部分 TCP 状态机与 Go 运行时(map、GMP、GC)是 IEG 服务端的高频区。