秋招季又到了,服务器开发工程师这个岗位的笔试,向来是淘汰率最高的一关。我前后帮学弟学妹们改了上百份笔试题复盘,也跟不少大厂面试官聊过出题思路,发现很多人挂在笔试题上,不是不会写代码,而是根本不知道这张卷子到底在考什么。畅游这类游戏公司的服务器开发岗,笔试风格很典型:不跟你绕弯子,题目看着基础,但每一道都在往深了挖,稍不注意就踩坑。这篇文章我就拿2024年秋招服务器开发工程师的笔试题作为引子,把这类岗位笔试背后的考点逻辑、常考题型、解题思路和避坑经验一次讲透,给正在准备秋招的你一份可以直接照着复习的路线。
先说清楚这篇文章适合谁看:目标岗位是服务器开发、后端研发、游戏服务端开发的应届生;已经拿到笔试通知但不知道怎么系统复习的同学;还有那些C++/Linux基础还行、但一遇到大题就发懵的朋友。不管你是科班还是半路转码,只要按着下面的思路把考点串起来,笔试这关会稳很多。
1. 先搞清楚:服务器开发工程师笔试到底在考什么
1.1 岗位能力画像:笔试是"能力雷达图"的第一次扫描
很多人以为服务器开发笔试就是考算法刷题,上来就猛刷LeetCode,结果进了考场发现题目跟想象中完全不一样。游戏公司也好,互联网公司也罢,服务器开发工程师这个岗位笔试的核心逻辑,其实是画一张你的"能力雷达图",考察的是你能不能在复杂的分布式环境下写出稳定、高效、可维护的服务器代码。
这个能力雷达图大概有五个维度:操作系统原理、网络编程功底、编程语言深度、数据结构与算法、系统设计思维。笔试题目不管怎么变,都是围绕这五个维度展开的。算法题只是其中一个维度,而且通常占比不超过30%。剩下那70%的分数,全压在你对底层原理的理解深度上。
为什么这么设计?因为服务器开发跟纯业务后端不一样。纯业务后端可能你搞清楚框架怎么用、数据库怎么连,就能干活了。但服务器开发面对的是成千上万的并发连接、毫秒级的延迟要求、7x24小时不宕机的稳定性要求。这时候你对操作系统怎么调度线程、网络数据包怎么在内核里流转、内存碎片怎么产生的这些底层机制的理解深度,直接决定了你写出来的代码是能抗压的工业级代码,还是只能跑通demo的学生作品。
1.2 高频考点权重与近年出题风向
我统计了2022年到2024年几十家公司的服务器开发笔试题目,发现考点分布其实相对稳定。这里分享一张可以当复习大纲的权重表:
| 考点模块 | 出现频率 | 常考题型 | 典型知识点 |
|---|---|---|---|
| 计算机网络 | 极高 | 选择题+简答 | TCP状态机、TIME_WAIT、粘包拆包、epoll原理 |
| 操作系统 | 极高 | 选择题+简答 | 进程线程协程、虚拟内存、死锁、多路复用 |
| C++/语言基础 | 极高 | 选择+代码输出 | 智能指针、虚函数、内存对齐、move语义 |
| 数据结构与算法 | 高 | 编程题 | LRU、TopK、海量数据、二叉树、DP |
| 系统设计 | 中高 | 简答+设计题 | 定时器设计、连接池、消息队列、并发模型 |
这里有个很明显的风向变化:早些年笔试题目会更偏向纯理论记忆,比如让你背TCP三次握手的过程、写个快排之类的。近两年明显更侧重"原理+应用"的结合,会给你一个业务场景,让你基于某个技术点去分析或者优化。比如"在高并发下TCP连接太多导致性能下降,你会怎么排查和处理",这种题目纯靠背书是答不好的。
还有一个小趋势是协程和io_uring这类相对较新的技术开始出现在选择题里。出题人不会要求你写过,但至少要知道它们和传统线程模型、epoll模型的区别。这说明考察的不是你背了多少八股,而是你平时有没有真的在关注技术演进。
2. 核心基础模块逐个拆解:从送分题到拉分题
2.1 操作系统:进程线程与内存管理是送分题也是送命题
操作系统这块的题目,表面上看都是经典八股,但出题人特别喜欢在细节上挖坑。先说进程、线程、协程这个万年不变的话题。基础版本问区别,进阶版本会问:一个进程崩溃会不会影响其他进程?一个线程崩溃会不会拖垮整个进程?前者不会,因为进程有独立地址空间;后者大概率会,因为同一进程内的线程共享地址空间。但如果你在选择题里看到"线程崩溃一定导致进程退出",这个"一定"就是坑,因为有些语言的运行时会在线程异常时做隔离处理,C++里未捕获异常调用std::terminate会导致进程终止,而Java里线程抛出未捕获异常默认也只是终止当前线程。
内存管理是另一个高频拉分点。虚拟内存、页表、缺页中断、内存映射这些概念,建议你串成一条线来理解:进程看到的是虚拟地址空间,CPU通过MMU查页表把虚拟地址翻译成物理地址,访问的页不在内存里就触发缺页中断,由内核从磁盘换入。这条链路讲清楚,相关选择题基本都能对付。
这里有个实践性很强的小知识点容易被忽略——内存对齐。笔试选择题里经常会给你一个结构体,让你算sizeof等于多少,然后给出一堆干扰选项。比如:
struct Test { char a; // 1字节 int b; // 4字节 char c; // 1字节 };如果你按字节数相加,得到6,但答案是12(64位系统默认对齐下)。因为编译器会把a后面填充3个字节让b对齐到4字节边界,c后面再填充3个字节让整个结构体大小是最大成员对齐数的整数倍。这个考点很基础,但每年都有人丢分,因为平时写代码不关心内存布局,一旦涉及网络协议打包、共享内存设计,内存对齐错误会导致严重问题。
2.2 计算机网络:TCP状态机是必考大头
网络这块,TCP就是半壁江山。三次握手、四次挥手的流程要烂熟于心,但这个层面已经拉不开差距了。真正拉开差距的题目长这样:为什么TIME_WAIT状态需要等待2MSL?如果服务端大量出现TIME_WAIT该怎么处理?这两个问题背后考的是你对TCP设计初衷的理解。TIME_WAIT等待2MSL,一是为了保证最后一个ACK能到达对端,如果丢了可以重传;二是确保本次连接的报文段在网络中完全消失,不会干扰下一个相同四元组的连接。大量TIME_WAIT常见于高并发短连接场景,处理手段包括开启tcp_tw_reuse、调整tcp_max_tw_buckets、改用长连接等。
再比如粘包和拆包问题,选择题喜欢考,简答题也喜欢考。很多人只知道要"解决粘包",但不理解粘包的本质是TCP是字节流协议,没有消息边界,所以需要应用层自己定义边界。三种主流方案要能说出来:固定长度消息、分隔符、消息头中携带长度字段。如果让你设计,你选哪个?游戏服务器一般选第三种,因为在效率和通用性之间最平衡,头部4字节存长度,后面跟消息体,解码时先读头部就知道要读多少字节。
还有epoll,简直是服务器开发的灵魂考点。常考的点包括:epoll和select/poll的区别、水平触发LT和边缘触发ET的区别、为什么epoll在高并发下性能更好。回答的关键在于指出select/poll每次调用都要把fd集合从用户态拷贝到内核态,并且需要线性扫描全部fd才能找到就绪的;而epoll通过红黑树管理fd、就绪链表记录就绪事件、epoll_wait只返回就绪的fd,不用全量扫描。ET模式相比LT模式通知次数更少、效率更高,但要求你一次把数据读完,否则会丢数据,所以实际工程里很多服务器为了简单可靠还是用LT。
2.3 C++语言深度:智能指针与内存安全是核心考点
服务器开发笔试里,C++相关的题目占的比重很大,尤其是游戏公司。这块考的不仅仅是语法,更是你对内存安全和性能的理解。
智能指针是绝对高频考点。先说shared_ptr的线程安全性,这个问题能刷掉一大批人。shared_ptr本身的引用计数是线程安全的,但它指向的对象不是线程安全的。也就是说,多个线程同时拷贝shared_ptr不会导致引用计数错乱,但多个线程同时修改shared_ptr指向的对象,需要你自己加锁。笔试简答题如果问你"shared_ptr是否线程安全",标准答法就是把这个区别说清楚。
还有个经典坑:shared_ptr循环引用导致内存泄漏。我见过好多次笔试题,给一段代码,让你分析有没有内存泄漏,代码里就是两个对象互相持有shared_ptr。正确的做法是其中一边改成weak_ptr打破循环。这个知识点光背结论不行,你得能画出引用计数变化的图,解释为什么weak_ptr能解决这个问题——因为weak_ptr不增加引用计数,它只是弱引用,需要时通过lock()升级成shared_ptr。
移动语义和完美转发在笔试里出现频率也在上升。选择题常考:下面哪些操作会触发移动构造而不是拷贝构造。出题人最喜欢放的坑是return局部对象,这是NRVO/移动语义的经典场景。而完美转发常配合模板来考,核心就是std::forward和引用折叠规则。这块建议你在复习时实际写几段代码跑一跑,光看书很难形成直觉。
2.4 数据结构与算法:不只刷题,更要懂工程应用
算法题在服务器开发笔试里依然要考,但风格跟纯算法岗有明显区别。纯算法岗爱考难题、偏题,服务器开发的算法题更接地气,经常把数据结构和真实场景结合起来。
LRU缓存是出现频率最高的题目之一,手写LRU几乎是必考题。要求实现get和put操作,时间复杂度O(1)。标准解法是哈希表+双向链表。为什么是双向链表而不是单向?因为删除某个节点时需要知道它的前驱节点,单向链表没法O(1)完成。这个"为什么"经常是面试追问的点,笔试时虽然不用写出来,但你心里得清楚。
再比如TopK问题,海量数据找最大的K个数,经典解法是大小为K的小顶堆。但笔试进阶版会问:如果数据量大到无法全部加载进内存怎么办。这时候要答分治+堆,或者HashMap统计后堆排序。这种题考的不只是你会不会堆,而是你有没有处理海量数据的工程直觉。
还有一个容易忽视的考点是字符串处理。服务器开发里协议解析、日志分析都离不开字符串,笔试里写一个高效的字符串分割函数、实现一个简单的JSON解析器,这类题目每年都有。建议你平时多练练手写字符串函数,不要一上来就调库,因为笔试环境有时候会限制你使用某些高级API。
3. 典型真题实战:从读题到AC的完整思考过程
3.1 网络编程题:设计一个高性能定时器
这是我很推荐大家认真做的一道经典题,因为出题人用一道题就串起了数据结构、操作系统和网络编程三个模块。题目大概长这样:请设计一个用于网络服务器的定时器,支持添加定时任务、取消定时任务、检测超时并执行回调,要求高效处理海量定时任务。
拿到这种题,千万不要上来就写代码。先明确需求:网络服务器里的定时器,最典型的应用场景是检测连接超时,比如30秒心跳没收到就断开连接。服务器可能同时有几十万个连接,每个连接一个定时器,这就要求添加、删除、超时检测三个操作都要尽量高效。
常见的方案有三种:最小堆、时间轮、红黑树(或用std::map)。如果你对Linux内核够熟悉,还会提到内核定时器用的就是最小堆。最小堆的优点是实现简单,添加和删除是O(logN),缺点是删除任意定时器需要先找到它,而堆里查找不是O(1),所以通常要配合一个哈希表存位置。时间轮的优点是添加和删除都是O(1),缺点是处理大跨度超时时间比较麻烦,可能需要多层时间轮。
我建议你笔试时选最小堆+哈希表的方案,因为思路清晰、容易写对,面试官也好理解。核心代码框架大概是这样:
class Timer { public: int64_t expire_time; // 到期时间戳(ms) int64_t id; // 定时器ID function<void()> callback; }; class TimerManager { private: vector<Timer> heap_; // 小顶堆 unordered_map<int64_t, size_t> index_; // id -> 堆数组下标 public: void addTimer(int64_t expire, function<void()> cb); void cancelTimer(int64_t id); vector<Timer> getExpired(int64_t now); };这里有一个非常容易踩的坑:vector扩容会导致元素的堆下标失效。因为你存的是index_[id] = 下标,如果vector重新分配了内存,虽然下标不变,但如果你把元素存成了指针,就会变成悬垂指针。所以要么存下标不存指针,要么写个堆模板,支持在交换元素时更新反向索引。笔试时间有限,你选方案时就该选自己最不容易出错的,不要为了炫技写多层时间轮结果调试半天。
3.2 海量数据题:10亿个整数中找出不重复的数字
这道题有多个变体,核心思路是一样的。10亿个整数,假设用int存储,就是40亿字节,约4GB,一台普通的笔试机器内存可能只有8GB,但你不能假定内存足够大,出题人就是要考你在内存受限下怎么处理。
思路要分几步展开。一种方案是位图法,每个整数用2个bit表示状态:00表示未出现,01表示出现一次,10表示出现多次。10亿个整数需要20亿个bit,约250MB,完全能装进内存。遍历一遍整数,通过位操作更新对应状态,最后再扫一遍位图,找出状态为01的。写代码时注意位图的索引计算:index = num / 16,offset = (num % 16) * 2,因为一个16bit的short可以存储8个整数的状态。
如果整数范围更大,或者要求内存更小,可以用哈希分片:把大文件切分成多个小文件,比如按整数的模1024分成1024个小文件,每个小文件单独统计,最后合并结果。这个思路值得写一下,因为它是MapReduce思想的雏形,考官会认为你有处理海量数据的工程视野。
这道题我特别说一句:笔试时一定要先写思路再写代码。很多阅卷人先看思路,思路对了哪怕代码有小bug,也可能给大部分分数。反过来,代码写得飞快但没写思路,一旦阅卷人没看懂你的算法,容易被判错。
3.3 系统设计题:单机IM的消息可靠投递
服务器开发笔试的系统设计题通常不会让你设计一个完整的分布式系统,更多是让你设计一个单机或者简单集群下能跑通的模块。我印象很深的一道题:设计一个单机IM服务器的消息可靠投递机制,要求消息不丢、不重,接收方离线时消息暂存。
这题的核心考点有三个:消息确认机制、重传机制、离线消息存储。参考设计是这样的:客户端A发消息给客户端B,消息带全局唯一ID,服务器先落库并记录状态为"待确认",然后推送给B,B收到后回ACK,服务器把状态改为"已确认"。如果A没收到服务器的确认,超时后会重发,而服务器根据消息ID去重,保证不重复投递给B。B离线时,消息存在数据库,等B上线后按时间拉取,拉取成功后再更新状态。
这道题可以顺便回答TCP和UDP的选择问题。IM场景一般用TCP或者基于UDP封装的可靠协议,比如QUIC就是基于UDP的可靠传输。如果你答用UDP,一定要说明应用层怎么实现可靠和有序,否则考官会认为你只是听说过UDP快,却不知道怎么保证可靠性。
我建议你在笔试时画一张简单的流程图或用文字把消息发送到接收的全链路描述清楚,先把流程讲对、讲全,再谈优化。因为系统设计题本质考的是你的工程思维,不是考你背了多少高深组件。
4. 备考路径与时间分配:从零到笔试合格的实操路线
4.1 复习优先级排布:先保底线再冲高分
秋招备考最大的问题是时间不够。很多人既想刷算法、又想补基础、还想做项目,结果什么都准备了一点,什么都不扎实。我给你的建议是分三轮复习,每轮有明确的目标。
第一轮(耗时约2周):主攻高频基础选择题和简答题。这个阶段的核心目标是保底线分数,把计算机网络、操作系统、C++语言这三个模块的基础概念吃透。每天花2小时看知识点,1小时做练习题。重点覆盖:TCP三次握手四次挥手、TCP vs UDP、进程线程区别、虚拟内存、智能指针、虚函数原理、内存布局。这轮结束,你应该能做到任何一道基础选择题都能解释清楚选项为什么对、为什么错。
第二轮(耗时约2周):主攻算法编程题和网络编程大题。每天至少手写2道题,重点题型包括:LRU、TopK、链表操作、二叉树遍历、字符串处理、动态规划入门。服务器开发的算法题整体难度低于纯算法岗,考的是基础熟练度。同时把epoll、select/poll、Reactor模式、定时器实现这些网络编程大题练熟,做到能默写核心框架的程度。
第三轮(耗时约1周):刷真题和模拟题,查漏补缺。这个阶段不建议再搞新知识,把之前做错的题、没搞懂的概念集中过一遍。同时严格按照笔试时间做2~3套模拟题,训练时间分配。我会特意练一练"不会的题先跳过"这个技能,因为笔试最怕的就是一道题卡住,后面的题目全没时间看。
4.2 工具书与资料选择:少而精,不贪多
市面上的资料太多了,但真正高效的是把少数几本经典吃透。操作系统方面推荐看《深入理解计算机系统》和《现代操作系统》的关键章节,前者讲虚拟内存和异常控制流非常清楚,后者讲进程线程讲得透彻。网络方面《TCP/IP详解卷一》是绕不开的,但不要逐页读,重点看TCP协议、超时重传、连接管理这几章。《Unix网络编程》不用全看,把epoll相关的章节看懂就行。
C++方面,《Effective Modern C++》和《STL源码剖析》二选一即可,我更推荐前者,因为2024年的笔试已经不流行考STL源码级别的细节了,更看重你对现代C++特性的理解。算法方面《剑指Offer》和LeetCode热题100足够了,服务器开发不需要你去抠偏题怪题。
资料使用的原则是:每个知识点建立"概念+代码+应用场景"的铁三角笔记。比如学epoll,先写下epoll和select的区别,再默写一个基于epoll的简易Reactor核心代码,最后标注出它的典型应用场景和坑(ET模式下必须循环读直到EAGAIN)。这个笔记到笔试前就是你的速查手册。
4.3 项目经验怎么转化成笔试优势
我经常遇到同学说:"我做过XX项目,但笔试还是不会做。"问题在于项目经验和笔试之间缺少一道"翻译"过程。你做过一个高并发服务器项目,这个经历很值钱,但你要学会把项目里的技术点翻译成笔试考点的语言。
举个例子,如果你的项目里用了epoll来管理多路IO,那么你就应该能回答这些问题:为什么用epoll而不是多线程每连接一个线程?生产者和消费者模型怎么设计的?怎么避免某个连接霸占CPU?如果你的项目里做了连接超时管理,那你就应该能画出定时器模块的结构图,讲清楚时间轮和最小堆的取舍。
我建议你专门花半天时间,把自己项目里用到的每一个核心组件列出来,然后用笔试风格去追问"为什么"和"如果极端情况怎么办"。这种转换训练比你盲目多做一个项目有效得多,因为你是在把已有的经验沉淀成可复用的知识框架。
4.4 时间分配:兼顾学校和笔试的节奏
秋招期间你大概率还在学校,可能有课程、论文的压力。这时候最忌讳的是三天打鱼两天晒网。我自己的经验是,保持每天固定的3小时学习时间比周末突击10小时效果好得多,因为知识和知识之间的连贯性很重要,断了一天再捡起来,光"回忆"就要花掉不少时间。
每周可以做一次模拟笔试,找一个安静的角落,按真实笔试的时间长度限制自己,手机放一边。这个训练的目的不是做题,而是练节奏和心态。你会发现真实笔试时最大的敌人不是题目难,而是前10分钟手忙脚乱、中间卡题心态崩、最后时间不够慌着交卷。提前演练能帮你把这些问题暴露在家里,而不是考场上。
4.5 考前一周的冲刺清单
考前一周不要再追逐新知识点了,把精力放在这些事上:第一,把高频选择题的错题全部重做一遍,确保错过的知识点不再错;第二,手写两遍核心代码模板,包括LRU、线程池、Reactor框架、自定义内存池;第三,把TCP状态图完整默画一遍,注意标出每个状态转移的触发条件;第四,对着自己的知识笔记快速过一遍,遇到讲不清楚的知识点马上查资料补上。最后一天,不要再做题了,早点睡,养足精神。笔试不仅考知识,也考状态,这个很多人忽略。
5. 考场发挥与避坑实录
5.1 笔试中那些防不胜防的"坑"
我在改别人的笔试题时发现,有些同学挂在同一个类型的坑上。第一个坑是审题不仔细。比如题目说"请实现一个线程安全的单例模式",很多人直接写教科书上的懒汉式加锁版本,却忽略了C++11以后可以用std::call_once或者函数局部static变量实现更优雅的线程安全单例。这种题考的是你知识有没有更新,不是会不会背历史版本。
第二个坑是多线程编程题忘记考虑竞态条件。笔试里经常给你一段多线程代码,让你分析结果或找bug。很多代码乍一看没问题,但仔细看会发现存在数据竞争。比如两个线程同时对一个全局变量做自增操作,由于自增不是原子操作,可能会丢更新。正确解法是用std::atomic或者加锁。这提醒我们在分析多线程代码时,脑子里要有一个检查清单:共享数据在哪?有没有同步?同步粒度够不够?有没有死锁风险?
第三个坑是内存管理题里藏着悬垂指针。有一段代码,函数返回了局部变量的地址或者引用了已经释放的内存,问程序会发生什么。正确回答要说清楚这是未定义行为,可能出现各种诡异现象,不能简单地回答"输出某个值"。笔试里这种题考察的是你对未定义行为的敏感度。
还有一个很隐蔽的坑是字节序。网络传输用的是大端序,而x86机器是小端序,写代码时如果不做转换,解析出来的数字就是错的。笔试选择题如果在网络协议相关代码里看到数值赋值,多留个心眼,看看有没有字节序转换的处理。
5.2 大题写不完怎么办
服务器开发的笔试题量通常不小,选择题+简答题+两三道编程大题,两小时内要全部高质量完成确实很难。我的策略是:先扫一遍所有题目,按"会做且快"→"会做但慢"→"不会做"三档分类。优先把第一档的分数稳稳拿住,然后做第二档,不会做的题哪怕蒙也尽量写点思路上去,不要空着。
简答题不要小看"写思路"的价值。很多简答题是看要点给分的,你写了关键步骤就有分。比如设计题你哪怕没有完整方案,但画出了关键模块的交互流程,阅卷人可能也会给一半分。编程题如果时间不够,至少把核心数据结构和关键函数的思路写在注释里,这样即使代码跑不通,考官也能看到你的代码有逻辑。
还有一个很实用的小技巧:善用题目给出的类和函数签名。笔试平台通常会提供一些辅助函数或者已经定义好的数据结构,顺着它们的思路往下写,往往就是出题人想要的解法。不要在外面自己另起炉灶定义一套结构,反而容易和给定的接口不匹配。
5.3 心态与节奏:把笔试当成一次技术交流
我知道很多人一到笔试就紧张,尤其是看到旁边人噼里啪啦敲键盘的声音就更慌了。这里分享一个有用的心态转换:把笔试当成一次跟出题人的技术交流,而不是一场审判。出题人设计这些题目,不是为了刁难你,而是想看到你怎么思考、怎么解决问题。你写思路的过程,就是在跟他对话。
遇到不会的题,我的处理方式是先做三个深呼吸,告诉自己"这题不会很正常,先记下来跳过去,回来再收拾它"。实践表明,很多时候做完后面题目再回头,思路反而打开了,因为大脑在后台还在后台处理这个问题的信息。
时间管理上,我给自己定的规则是:选择题平均每题不超过1分钟,简答题每题不超过10分钟,编程大题每题控制在25分钟以内。如果一道题超过规定时间还没突破,立刻做标记跳下一题。笔试结束前留出10分钟检查,重点看有没有漏答、代码有没有明显的编译错误。
5.4 笔试之后:复盘比成绩更重要
笔试结束不是终点,复盘才是提升最快的方式。我会建议你不管考得好不好,都在考后第一时间把题目回忆出来,哪怕记得个大题方向也行,然后逐个知识点对照自己的掌握情况做标记。标记成三类:完全掌握的、有点模糊的、完全不会的。
对于"完全不会"的知识点,如果它是高频考点(比如TCP、epoll、线程池),立刻安排时间补上,因为面试环节大概率还会追着问。对于"有点模糊"的,重新翻看资料,然后自己给自己讲一遍,能流畅讲出来才算过关。很多同学考完试就把题丢了,然后下一场笔试继续踩同一个坑,实在太可惜了。
我个人还有一个习惯:把每次笔试的错题整理成一个文档,按知识点分类,每周翻一遍。秋招季投的公司多、笔试多,这个文档就是你的个性化复习资料,比任何市面上的题库都更有价值。
说到底,笔试只是秋招这条路上的一关。它会暴露你的知识短板,但也给了你一次清晰的自我诊断机会。哪怕这次没考好,把坑都记下来,下一次就是进步。把心态放平,按照上面这些思路踏实准备,我相信你能在服务器开发工程师的笔试里发挥出自己真正的水平。