拼多多客户端开发一面:TCP 保活、登录鉴权与 C++ 智能指针
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 你对移动客户端的了解有哪些?
- 简述一下游戏项目中的登录和创角流程?
- 玩家断线怎么处理?每次玩家登录都会重新建立 TCP 连接吗?
- TCP 的保活机制是怎样的?HTTP 的长连接机制是怎样的?
- 项目目前处在初期开发阶段,用的是 HTTP 还是 HTTPS?为什么?
- TCP 的三次握手和四次挥手是什么?为什么需要四次挥手而不是三次?
- 用户每次携带账号和密码登录都需要访问数据库吗?如果多用户同时登录,怎么避免数据库被打爆?
- 移动端开发通常会使用多种语言,你了解多语言之间如何通信吗?
- C++ 的 new 做了什么?如果 new 过程中失败了会发生什么?
- C++ 的智能指针有哪些?各自解决了什么问题?shared_ptr 是线程安全的吗?
- C++ 的 lambda 底层原理是什么?
- 算法题:岛屿数量的变式。
- 算法题:一道贪心。
《参考解析》
- TCP 保活与 HTTP 长连接不是一回事:TCP keepalive 是内核的探测机制,连接空闲超过
tcp_keepalive_time(默认 7200 秒)后开始发探测包,按tcp_keepalive_intvl的间隔探测tcp_keepalive_probes次都没响应就判定对端已死并关闭连接。默认两小时才触发,对「秒级感知掉线」远远不够,所以客户端和服务端一般另做应用层心跳(定时 ping、超时未收就判离线并清理会话),再配合SO_KEEPALIVE、TCP_USER_TIMEOUT这类选项缩短感知时间。HTTP 的 keep-alive 则是应用层的连接复用:响应头声明这个 TCP 连接不关,后续请求复用同一条连接省掉握手与慢启动开销,HTTP/1.1 默认开启,连接的存活时间由服务端空闲超时或Keep-Alive: timeout决定。一句话区分:一个在判断「这条连接还活着吗」,一个是让连接被反复使用。 - 三次握手与四次挥手:握手是客户端发 SYN 带上自己的初始序列号,服务端回 SYN+ACK(确认对方的号并给出自己的号),客户端再回 ACK,三次的目的是让双方都确认「自己能发、对方能收」以及彼此的初始序列号。挥手是四次,因为 TCP 是全双工:主动方发 FIN 只表示自己不再发数据、但仍能接收,被动方要先回 ACK,等自己剩余数据发完再发 FIN,主动方最后 ACK 并进入 TIME_WAIT。ACK 与 FIN 不能随意合并,只有在被动方没有数据要发时才可能合并成三次。
- 登录与创角流程:客户端带着账号凭证去登录服校验(密码或第三方渠道票据),通过后返回会话 token 和角色列表;选服时再拉该服的角色,没有角色就走创角——校验昵称唯一性、写角色基础数据、初始化属性与背包;最后凭 token 连网关进入游戏服。工程上登录服与游戏服分离,网关统一维持长连接,游戏服只处理逻辑,token 有有效期并支持续期。
- 断线处理与要不要重建 TCP:服务端靠心跳超时判离线,但不立刻销毁角色——留一段宽限期,既让客户端有机会重连,也避免网络抖动导致的状态回滚;战斗中断线一般做托管或冻结,避免角色凭空消失或被打死。客户端做自动重连并采用指数退避,重连成功后走「重登 + 状态恢复」:用会话 token 换一个当前状态快照或增量事件补齐断线期间的变化。正常游玩期间连接是复用的,不会每次登录都重新握手;只有断线、切网络或长时间空闲被服务端回收之后才会重新建连,所以协议设计上要假设「连接可能随时失效」,用 token 而不是连接身份来标识玩家。
- 登录鉴权怎么样才不把数据库打爆:不可能每次请求都去查库校验密码。做法是登录时校验一次,之后签发有有效期的 token(JWT 靠签名校验、自建 session 存 Redis),后续请求只验 token,只有敏感操作或 token 过期才回源;账号与角色数据放 Redis 缓存。再叠几层防护:按 IP 和账号限流、登录失败次数锁定与验证码、连接数与请求频率上限、按账号分片把热点散开,数据库本身读写分离、加缓存、必要时连接池与队列削峰。另外密码永远不存明文,存加盐哈希(bcrypt / scrypt / Argon2)。
- HTTP 还是 HTTPS:内网联调或纯本地开发用 HTTP 图个快是可以的,但只要涉及真实用户的账号密码和登录态就必须上 HTTPS——它提供加密(防窃听)、完整性校验(防篡改)和服务器身份认证(防中间人),对移动端尤其重要,因为公共 Wi-Fi 和代理抓包非常普遍。客户端侧还要做服务端证书校验、必要时做证书固定,禁止无脑信任所有证书(
trustAll)的实现。现在证书有免费方案,成本不构成继续用 HTTP 的理由。 - 移动端多语言如何通信:Android 上 Java/Kotlin 与 C/C++ 走 JNI,Flutter 与原生走 MethodChannel 或 dart:ffi,iOS 上 Swift/OC 与 C++ 通过 Objective-C++ 或纯 C 接口桥接;跨进程用 Binder/AIDL,WebView 与原生用 JSBridge,跨设备或跨端统一走序列化协议(Protobuf / JSON)加 socket。共同约束是跨语言边界只传 POD 或序列化数据,不要把带生命周期的对象裸传过去;边界要收敛在一层薄封装里,线程模型要显式约定(JNI 里从哪个线程回调、要不要 attach),异常不能跨越语言边界传播。
- C++ 的 new 与失败行为:
new T(args)分两步——先调用operator new分配内存,再在这块内存上调用构造函数。默认版本的分配失败会抛std::bad_alloc(用new (std::nothrow)才是返回空指针),而构造过程中抛异常时已经分配的内存会被自动释放,不会泄漏。new[]分配的数组必须用delete[]释放,两者不能混用;placement new 只负责在已有内存上构造对象,内存由调用方管理。嵌入式或内存紧张场景下才考虑 nothrow 版本并在每处判空。 - 智能指针与线程安全:
unique_ptr独占所有权,零额外开销,不可拷贝只能移动,是默认首选;shared_ptr用引用计数共享所有权,控制块在堆上(make_shared可以合并成一次分配),weak_ptr用来打破循环引用并配合lock()判断对象是否还活着;auto_ptr已废弃。shared_ptr的线程安全性要分开讲:引用计数的增减是原子的,不同线程各自持有副本去拷贝析构是安全的;但它指向的对象本身不是线程安全的,而且同一个shared_ptr实例被多个线程同时读写(赋值或 reset)依然是数据竞争,需要外部同步或用std::atomic<std::shared_ptr>。要在对象内部安全地取到自己的shared_ptr得继承enable_shared_from_this。 - lambda 的底层原理:编译器为每个 lambda 生成一个唯一的匿名闭包类,捕获列表里的变量成为它的成员(按值捕获是拷贝、按引用捕获是引用,
mutable决定operator()是否为 const),调用 lambda 就是调用这个类的operator()。无捕获的 lambda 可以隐式转换成普通函数指针,所以能直接传给 C 风格回调。要注意按引用捕获的变量在 lambda 生命周期外被销毁会悬垂,异步回调里捕获this或局部引用是常见事故点。 - 岛屿数量及其变式:基础做法是遍历网格,遇到陆地就计数加一,然后用 DFS 或 BFS 把与之相连的陆地全部标记为已访问;规模大时用显式栈或队列避免递归爆栈,或者用并查集把相邻陆地 union,最后数集合个数。常见变式:最大岛屿面积(遍历时累加面积取最大值)、封闭岛屿(先把边界上的陆地连同其连通块淹掉,再数剩下的)、不同岛屿的数量(记录遍历路径的方向签名去重,若旋转镜像算同构还要做归一化)、岛屿周长(统计每条陆地边中邻接水域或越界的边数)、以及陆地逐块出现时在线返回数量(只能并查集)。答题前先把连通定义(四连通还是八连通)、能否修改原数组、数据规模这三个前提确认清楚。
- 贪心题:面试里的贪心通常要能说清「局部最优为什么能推出全局最优」,常见套路有区间调度按右端点排序、跳跃游戏维护当前可达最远位置、分发问题里的排序后双指针、以及带反悔机制的优先队列解法(先贪心选,出现更优选择时把之前的换掉)。写之前先把贪心策略和交换论证讲一遍,确认过不了再退到 DP。