不知道你有没有遇到过这样的题目:题目只有一句话——“Java面向对象-14 网络编程(主观题)”,剩下的全靠自己发挥。我最初是在一套实训题单里见到的,后来陆陆续续在不少Java课程设计和招聘笔试题里看到了同款考法。这类题目本质上不是让你背“TCP和UDP区别是什么”,而是要你在一个真实的网络编程场景里,用面向对象的方式把功能做出来,并且在代码之外还能讲清楚“你为什么这么设计”。说白了,它考的是你同时吃透了两块东西:Java面向对象的设计能力,以及基于Socket的网络编程基本功。
这类主观题对很多人来说是噩梦,因为题目看着很简单,但真要动手写,你会发现满脑子都是“我该建几个类”“accept之后怎么同时服务多个客户端”“消息发到一半断了怎么办”。这篇文章我就围绕这类题目展开,从考点拆解、解题思路、完整可跑的Demo,再到高频踩坑记录,把我自己反复写过、也帮别人改过的那套经验全部放出来。无论你是准备课程设计、期末考试,还是Java方向面试,看完之后至少能建立起一套应对这类题目的稳定框架。
1. 这类主观题到底在考什么
1.1 主观题和客观题的本质差别
先说说“主观题”这三个字的分量。选择题、填空题考的是记忆,你知道TCP三次握手、知道Object类有哪些方法,基本就能得分。但主观题不一样,它通常要求你“实现一个功能”,或“阅读一段代码后回答问题”。背后根本的差距只有一句话:你能不能把知识变成设计。
我见过很多人的代码能跑通,但设计得一塌糊涂。所有逻辑全写在main方法里,一个类从头写到尾,accept一个连接就while循环处理消息,结果第二个客户端连不上。这种代码就算功能是好的,在主观题评分体系里也很难拿高分,因为阅卷人不是看你的功能演示,而是看你的类职责划分、接口抽象、异常处理、资源管理,这些才是真正的得分点。
所以解读这类题目,我的建议是:先把它看作一道“架构题”,再看作一道“功能题”。功能只是下限,架构才是上限。
1.2 考点分布:面向对象到底体现在哪几个环节
把“Java面向对象”和“网络编程”叠在一起,常见的考点其实非常固定。我整理了一份自己的考点清单,实战中命中率很高:
- 封装:网络消息体有没有封装成类?Socket、IO流有没有被包在一个会话类里管理?还是说到处都是裸的InputStream和OutputStream?
- 接口隔离与多态:不同的消息类型(登录、群聊、私聊、退出)是否通过统一的Handler接口处理,还是用一坨if-else硬堆?
- 线程安全设计:在线用户列表用什么集合?多个线程同时给不同客户端写数据,需不需要同步?
- 资源管理与异常路径:所有流和Socket是否可靠关闭?客户端突然离线,服务端能不能感知并清理?
- TCP/UDP、I/O模型等技术理解:为什么选TCP、为什么用BIO而不是NIO,这种知识题经常以“说明理由”的形式出现。
换句话说,网络编程只是载体,核心还是在考察面向对象。题目可能换一千种花样,比如文件传输、HTTP报文解析、简易聊天室,但骨架永远是“网络通信 + 多线程 + 对象建模”。
1.3 拿到题目后最先想清楚的三件事
我每次拿到这类题目,不管题目多长,都先圈定三个问题:
第一,通信模型是单客户端还是多客户端。多客户端就意味着服务端必须并发处理,多线程或线程池马上要提上日程。第二,消息格式如何自定义。纯文本按行读自然简单,但遇到二进制文件或数据量大的场景,就要考虑粘包半包问题。第三,哪些职责是可以从主体流程里拆出去的。比如“接收消息”是一个类,“解析消息”是一个类,“路由消息”又是一个类,这样每个类都短小精悍,评分时也好看。
只要这三件事有了答案,代码基本就出来了半截。
2. 题目拆解:五种常见考法与应对思路
这类主观题虽然描述千奇百怪,但落地之后基本跑不出下面几种考法。整理出来,某种程度上你等于把题库的底牌提前看了。
2.1 多人聊天室/群聊服务
这是最常见的考法,也是我认为最适合练手的题型。要求一般是:多个客户端可以连接同一个服务器,任意一个客户端发送的消息会被广播给所有人,支持私聊或者登录昵称。要说难点,核心就在“多客户端并发读写”和“消息广播时对在线列表的并发遍历”。
设计上我会把在线用户列表定义为ConcurrentHashMap<String, Session>,key是用户名,value是封装了Socket和读写器的会话对象。广播消息时遍历这个Map,逐一调用会话的发送方法。注意广播不要发给发送者自己,否则客户端本地要额外处理一次回显,容易出现重复消息。
2.2 文件传输与断点续传
这种考法比聊天室更偏底层一些,重点考察流的处理。客户端把文件传给服务器,服务器落盘。进阶一点会要求断点续传,即传输中断后从已经传过的字节偏移量继续传。
文件传输必须自己定一套基本传输协议,比如文件头先传文件名长度和文件名,再传文件总大小,最后传文件内容。服务端依次解析。断点续传的话,客户端先发送一个查询请求,服务端返回本地已有文件的偏移量,客户端从偏移量处继续读源文件并发送。这里我通常会给传输定义一个FilePacket类,包含fileName、fileSize、offset、data几个字段,实现序列化或自解析,比散装字节流清晰得多。
2.3 简易HTTP服务器
这种题会把网络编程和协议解析结合,难度直接上一个台阶。要求通常是:服务端监听8080端口,客户端用浏览器访问时返回一个简单的HTML页面;进阶一点要求解析GET参数、返回静态文件,甚至区分不同Content-Type。
做这类题最核心的是理解HTTP报文结构:请求行、请求头、空行、请求体。用一个HttpRequest类去解析原始字符串,把method、path、headers拆成字段,再设计一个HttpResponse类负责拼响应状态行和响应体。分清楚“协议解析”和“业务处理”两个层,这就是面向对象加分的点。
2.4 基于Socket的简易RPC调用
这类题在面试题里越来越常见,要么是让你模拟客户端远程调用一个“方法”,要么是让你写一个服务端注册中心。我见过一个题就是:客户端传一个方法名和参数给服务端,服务端执行本地方法后返回结果。
RPC考题里最容易出彩的地方是把“请求”设计成一个完整的对象,比如RpcRequest包含requestId、methodName、parameterTypes、parameters。服务端通过反射调用方法,返回RpcResponse。这种题能全面展示你对面向对象、反射、动态代理、网络传输的综合掌握,属于天花板级别的考法。
2.5 数据采集/客户端主动请求类
这类题型模拟的是爬虫或者API调用场景:客户端定期向服务端请求数据,服务端从数据库或内存中读取数据返回。常见于课程设计,比如“天气信息查询系统”“图书信息检索服务”。
这种题技术难度不高,反而更考察你如何处理多个请求参数、如何组织返回结果。我一般会把请求参数封装成QueryRequest对象,把结果封装成QueryResponse,服务端内部再用一个Service层去处理查询逻辑,而不是在Socket回调里直接操作数据。层次分明之后,就算题目升值加了用户认证也容易扩展。
| 考法类型 | 核心难点 | 首选技术方案 | 面向对象加分点 |
|---|---|---|---|
| 多人聊天室 | 并发读写、广播 | TCP + 线程池 | Session封装、Handler接口 |
| 文件传输 | 流解析、断点续传 | 自定义传输协议 | 请求/响应对象建模 |
| HTTP服务器 | 报文解析、路由分发 | TCP + 字符串解析 | Request/Response类 |
| 简易RPC | 反射调用、协议编解码 | 对象序列化 + 反射 | 请求/响应对象、代理 |
| 数据查询服务 | 参数解析、结果封装 | TCP或UDP | Service层封装、参数对象 |
3. 一个能打满分的多人聊天室Demo
理论说得再多,不如一个能跑的Demo。接下来我完整拆解一个多人聊天室的实现,这是我在处理这类主观题时最常用的模板,核心代码可以直接抄,但我会把每个设计动机都讲清楚,你以后改造成其他考法也顺手。
3.1 对象模型设计:从角色开始逐步拆解
拿到题目先别急着写ServerSocket,先在纸上把角色列出来。聊天室里有三个角色:服务端、客户端、消息本身。消息又分登录、群聊、私聊、退出、系统通知这几种类型。基于这个分析,我顶层设计这几个类:
ChatServer:启动服务端,监听端口,处理连接接入。ClientHandler:每个客户端连接对应一个线程,负责读消息并路由。Session:封装一个客户端连接,持有Socket、用户名和读写流。Message:消息对象,包含type、from、to、content、timestamp。MessageHandler:消息处理器接口,每种类型对应一个实现类。MessageCodec:消息编解码器,负责把Message转成字节流,从字节流中读回Message。
这里提一下Session的设计。很多人习惯直接把一个Socket丢到各个方法里到处传,导致参数列表越来越长,看不出来谁是谁。我把它收到一个Session类里,后面的所有方法都只认Session,这样干净很多,这也是封装的一个具体体现。
3.2 消息设计与编解码细节:粘包半包问题提前排掉
聊天室里客户端和服务端之间传递的是文本消息,但TCP是一个流协议,它不知道你一条消息在哪里结束。如果你直接发送字符串,很可能会发生粘包:两次write的数据粘在一起被对方一次读走。另一个是半包:一次预期的数据被拆成了多次到达。
业内最简单的解法之一就是“长度前缀”:发送方先写一个int类型的长度,再写对应长度的字节;接收方先读4个字节拿到长度,再循环读够长度。
public class MessageCodec { private final DataInputStream in; private final DataOutputStream out; public MessageCodec(Socket socket) throws IOException { in = new DataInputStream(socket.getInputStream()); out = new DataOutputStream(socket.getOutputStream()); } public void send(Message msg) throws IOException { byte[] data = toBytes(msg); out.writeInt(data.length); out.write(data); out.flush(); } public Message receive() throws IOException { int len = in.readInt(); byte[] data = new byte[len]; in.readFully(data); return fromBytes(data); } private byte[] toBytes(Message msg) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos)) { dos.writeUTF(msg.getType()); dos.writeUTF(msg.getFrom()); dos.writeUTF(msg.getTo() == null ? "" : msg.getTo()); dos.writeUTF(msg.getContent() == null ? "" : msg.getContent()); dos.writeLong(msg.getTimestamp()); dos.flush(); return bos.toByteArray(); } } private Message fromBytes(byte[] data) throws IOException { try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) { Message msg = new Message(); msg.setType(dis.readUTF()); msg.setFrom(dis.readUTF()); msg.setTo(dis.readUTF()); msg.setContent(dis.readUTF()); msg.setTimestamp(dis.readLong()); return msg; } } }上面用到的writeUTF已经包含了“前两个字节记录长度”的逻辑,所以很适合嵌套进我们自己的长度前缀方案里。整个编解码器封装好了之后,上层代码完全不用关心底层字节怎么拼,这也是我对“封装”二字的理解:调用方只面对一个简单的send(Message)和receive()。
3.3 服务端核心代码:线程池管理并发连接
服务端的启动逻辑很简单,就是循环调用accept()接收新连接。关键是接收之后如何处理。
很多新手会在accept之后直接while(true)处理该连接的消息,这样一旦进入循环就再也接收不到新客户端了。正确做法是把每个连接交给一个独立的线程处理,主线程只负责接收。为了控制资源,我不建议无限new Thread,而是用一个固定大小的线程池。
public class ChatServer { private final int port; private final ExecutorService pool; private final Map<String, Session> onlineUsers = new ConcurrentHashMap<>(); private final Map<String, MessageHandler> handlers = new ConcurrentHashMap<>(); public ChatServer(int port, int poolSize) { this.port = port; this.pool = Executors.newFixedThreadPool(poolSize); // 注册不同消息类型的处理器,体现面向对象的多态 handlers.put(Message.TYPE_LOGIN, new LoginHandler(onlineUsers)); handlers.put(Message.TYPE_CHAT, new ChatHandler(onlineUsers)); handlers.put(Message.TYPE_PRIVATE, new PrivateHandler(onlineUsers)); handlers.put(Message.TYPE_QUIT, new QuitHandler(onlineUsers)); } public void start() throws IOException { try (ServerSocket serverSocket = new ServerSocket(port)) { System.out.println("聊天服务器已启动,监听端口:" + port); while (true) { Socket socket = serverSocket.accept(); System.out.println("新客户端接入:" + socket.getRemoteSocketAddress()); pool.execute(new ClientHandler(socket, handlers, onlineUsers)); } } } public static void main(String[] args) throws IOException { new ChatServer(8888, 20).start(); } }注意到我用了一个Map<String, MessageHandler>按消息类型注册处理器。以后题目加了一个“发送文件”的消息,我只需要新增一个FileHandler并注册进去,一行不用改动原来的分发逻辑。这就是开闭原则的一个小实践。
3.4 客户端与消息分发:用Handler替代层层if-else
ClientHandler是每个连接的工作线程。它的职责简单说就是循环读取消息,然后交给对应的处理器。完整代码可以参考下面这样:
public class ClientHandler implements Runnable { private final Socket socket; private final Map<String, MessageHandler> handlers; private final Map<String, Session> onlineUsers; private Session session; public ClientHandler(Socket socket, Map<String, MessageHandler> handlers, Map<String, Session> onlineUsers) { this.socket = socket; this.handlers = handlers; this.onlineUsers = onlineUsers; } @Override public void run() { try { MessageCodec codec = new MessageCodec(socket); // 第一步一定是登录,这里直接等待第一条登录消息 Message msg = codec.receive(); if (!Message.TYPE_LOGIN.equals(msg.getType())) { socket.close(); return; } session = new Session(socket, msg.getFrom(), codec); onlineUsers.put(msg.getFrom(), session); System.out.println(msg.getFrom() + " 登录成功,当前在线人数:" + onlineUsers.size()); // 广播系统通知 Message notice = Message.newSystemMessage(msg.getFrom() + " 加入了聊天室"); broadcast(notice, null); while (true) { Message incoming = codec.receive(); if (incoming == null) { break; } MessageHandler handler = handlers.get(incoming.getType()); if (handler != null) { handler.handle(incoming, session); } } } catch (IOException e) { System.out.println("客户端连接异常:" + e.getMessage()); } finally { // 连接关闭时,把用户从在线列表移除 if (session != null) { onlineUsers.remove(session.getUserId()); try { socket.close(); } catch (IOException ignored) { // 关闭失败不需要再处理 } System.out.println(session.getUserId() + " 已离线"); } } } private void broadcast(Message msg, Session exclude) { for (Session target : onlineUsers.values()) { if (target != exclude) { try { target.getCodec().send(msg); } catch (IOException e) { // 单个客户端发送失败,不影响其他客户端 System.out.println("广播发送失败:" + e.getMessage()); } } } } }有一个细节很容易被忽略:handlers.get(incoming.getType())这一步意味着消息处理器可以是全局共享的,而session则是每个连接独立的。所以公共的处理器里如果需要操作某个具体的连接,必须通过参数传入的session,而不是自己保存一份全局引用。这样设计既能共享逻辑,又不会串连接。
3.5 消息处理器与Session封装
Session类应该是什么样的?我给它放了三个核心字段:Socket、用户标识、编解码器。
public class Session { private final Socket socket; private final String userId; private final MessageCodec codec; public Session(Socket socket, String userId, MessageCodec codec) { this.socket = socket; this.userId = userId; this.codec = codec; } public Socket getSocket() { return socket; } public String getUserId() { return userId; } public MessageCodec getCodec() { return codec; } }然后定义一个顶层的消息处理器接口:
public interface MessageHandler { void handle(Message message, Session session); }以群聊处理器为例,实现类只做一件事:把消息转发给除了自己之外的所有在线用户。
public class ChatHandler implements MessageHandler { private final Map<String, Session> onlineUsers; public ChatHandler(Map<String, Session> onlineUsers) { this.onlineUsers = onlineUsers; } @Override public void handle(Message message, Session session) { try { for (Session target : onlineUsers.values()) { if (!target.getUserId().equals(session.getUserId())) { target.getCodec().send(message); } } } catch (IOException e) { System.out.println("群聊消息发送失败:" + e.getMessage()); } } }私聊处理器则需要根据message.getTo()找到目标用户,只发送给那一个Session。这样每种消息类型的行为都被封装到了一个独立的类里,Main逻辑里看不到任何分支判断,这就是多态带来的可读性提升。
3.6 客户端实现:双线程读键盘与读服务端
客户端这边,我不能只写一个同步顺序的模型,否则会被阻塞在codec.receive()上,键盘消息根本发不出去。所以客户端的标准做法是开两个线程:一个线程负责读键盘并发送消息,另一个线程负责接收服务器消息并显示。
public class ChatClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8888); MessageCodec codec = new MessageCodec(socket); BufferedReader console = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); // 先登录 System.out.print("请输入昵称:"); String name = console.readLine().trim(); Message loginMsg = Message.newUserMessage(Message.TYPE_LOGIN, name, null, "login"); codec.send(loginMsg); // 接收线程 new Thread(() -> { try { while (true) { Message msg = codec.receive(); System.out.println("[" + msg.getFrom() + "]:" + msg.getContent()); } } catch (IOException e) { System.out.println("连接已断开"); } }).start(); // 发送线程(主线程) String line; while ((line = console.readLine()) != null) { if ("/quit".equalsIgnoreCase(line)) { codec.send(Message.newUserMessage(Message.TYPE_QUIT, name, null, "quit")); break; } // 私聊简单语法:@用户 内容 int idx = line.indexOf(' '); if (line.startsWith("@") && idx > 1) { String target = line.substring(1, idx); String content = line.substring(idx + 1).trim(); codec.send(Message.newUserMessage(Message.TYPE_PRIVATE, name, target, content)); } else { codec.send(Message.newUserMessage(Message.TYPE_CHAT, name, null, line)); } } socket.close(); } }这个客户端的发送逻辑走的是主线程,接收逻辑走的是子线程。实际运行中可能出现控制台输出和输入混在一起的情况,但对课程设计级别的聊天室来说完全够用。如果你愿意优化,可以把输出统一交给一个UI线程,但那是另一个话题了。
3.7 如果题目的并发量要求更高,下一步怎么升级
上面这套BIO + 固定线程池的方案,可以轻松应付几十个连接。但如果题目明说要求支持高并发,或者老师追问“你如何优化”,你得有升级思路。
我通常会给出三层演进方向。第一层是把每连接的线程模型改成NIO的非阻塞模型,用Selector统一管理多个Channel,这样不需要每个连接一个线程;第二层是引入Netty框架,把编解码、空闲检测、断线重连全都交给成熟的组件处理;第三层是引入消息队列,把广播逻辑从服务端线程里分离出来,做成异步消息分发。
但说实话,判分时很少指望你真写一个Netty聊天室出来。你只要能说清楚BIO的问题、NIO的思路、Netty能做哪些事,就已经超出大多数人了。
4. 常见问题与排查技巧实录
这类题目在实际开发或考试中遇到的问题是高度相似的。我直接把我踩过的坑和帮别人查过的案列整理成速查表,每一条都是实打实修过的。
4.1 客户端连接不上服务端
这个问题的排查优先级我认为是最高的。先检查服务端有没有启动,再看端口是否被防火墙拦截,最后看客户端连接的IP和端口是否写对。一个非常典型的坑是服务端绑定的是0.0.0.0或127.0.0.1,客户端却连了一个远程IP,结果当然是连接拒绝。
还有一个小细节:如果服务端已经启动过一次,没有正常关闭Socket,再次启动时端口可能还是被占用状态。这时候Linux下会报Address already in use,处理方式要么等一段时间让TIME_WAIT状态过去,要么在ServerSocket绑定端口前设置serverSocket.setReuseAddress(true)。
4.2 消息出现粘包或半包
在纯文本聊天室里,如果你不做长度前缀,只靠readLine(),遇到行尾没有换行符的情况就会出现半包;如果客户端一次write了两条消息,服务端就会粘包。解决方式我在上面的MessageCodec里已经展示了:统一走长度前缀。
你要记住,TCP是字节流,不是消息流。所有基于TCP的自定义协议,几乎都要自己定义“一条消息从哪里开始到哪里结束”。哪怕题目不要求,我也建议你提前加上,否则线上问题排查会让你怀疑人生。
4.3 多个线程同时写一个客户端连接
聊天室广播这类场景很容易出现两个线程同时操作同一个Socket输出流,比如用户A在群聊,用户B在私聊A,两条消息同时到达,最终在Connection里交错发送,导致对方解析消息时错乱。
解决方式有两种。第一种是给Session的发送方法加synchronized,保证同一时间只有一个线程在写;第二种是每个Session内部维护一个发送队列,由一个单线程的发送端负责顺序发送。课程设计用synchronized就足够了,但如果题目要求高性能,最好考虑队列方案。
4.4 客户端强制关闭导致服务端线程残留
如果客户端直接拔网线或者用Ctrl+C强制中断,服务端正在阻塞读取的readFully方法不会立刻返回,而是等到TCP连接超时后才会抛出SocketTimeoutException或EOFException。如果服务端没有做异常捕获,那么这个线程就一直挂着,占用线程池名额。
我的处理方式是客户端正常退出时必须发送QUIT消息,服务端收到后主动断开;强制退出这种异常情况则在catch块里统一做清理,比如移除在线列表中的用户、关闭Socket。测试阶段你可以用setSoTimeout(30000)来设置读取超时,这样即使对端静默断开,最多30秒后也会自动放弃阻塞。
4.5 主观题问答环节怎么答才能拿分
有些主观题不只是让你写代码,还会附赠几道问答。我把频率最高的一批题目直接列成答案,你背下来或者理解后用自己的话说都行。
问:为什么服务端要使用多线程而不是单线程?答:单线程中accept阻塞在接收连接时,无法同时处理已建立连接的数据通信,会导致多个客户端必须排队等待。多线程可以让每个连接的读写独立并行,提高响应速度和吞吐量。
问:TCP和UDP如何选择使用?答:TCP可靠、面向连接,适合文件传输、聊天、HTTP等要求数据完整性的场景;UDP无连接、开销小、延迟低,适合视频通话、实时游戏对战等可以容忍少量丢包的场景。
问:BIO、NIO、AIO有什么区别?答:BIO是阻塞I/O,读操作等待时线程阻塞;NIO是非阻塞I/O,基于Selector实现多路复用;AIO是异步非阻塞I/O,操作完成后由内核回调通知。BIO模型简单、适合连接数少的场景,NIO适合连接数多的场景,AIO适合大量读写操作且要求高性能的场景。
问:如何保证多线程操作共享数据的安全?答:可以通过synchronized关键字加锁、使用Lock显式锁、将数据放在ConcurrentHashMap等并发容器里,以及使用volatile保证可见性、Atomic类保证原子性。具体选择要看业务场景和数据竞争的激烈程度。
问:Socket通信为什么需要心跳机制?答:因为TCP本身在长时间无数据传输时可能无法立刻发现对端已经断开,心跳包周期性发送小数据,可以及时检测断线并清理废弃连接。
4.6 数字评分视角下最容易丢分的几个地方
作为写代码的人,我们总觉得自己功能做出来了就完美了。但换个角度,站在判分者那边看,他们最在意的是这几点:
第一,资源泄漏。有没有用try-with-resources,finally里有没有关流。这是很常见的扣分项,但也是最容易修好的。第二,代码可读性。一个文件超过三百行、变量全是a、b、c,分数直接减半。第三,没有注释。不是让你每行都写,而是在类头、方法头、关键逻辑处写清设计意图。第四,不做输入校验。比如客户端传来的用户名为空时,直接抛空指针。第五,功能与描述不符。题目要求支持私聊,结果你只实现了群聊广播。
如果你在写完功能之后能花半小时做一轮自查,把上面这五点逐条过一遍,我认为分数会比大部分人高一个档位。
5. 给刷题者的一些实操心得
经常有人问我这类题目到底要练到什么程度才算过关。我的标准很简单:不看任何资料,能在90分钟内从零写出一套可运行的聊天室或文件传输程序,并且能说清楚每个类的职责和每个技术选型的理由。
练习方式是先刻意不查资料写一遍,写到哪卡住就在哪里停下来,把这个卡点记录下来,然后针对性地去查资料、看代码,再重写一遍。第二遍最好能做到比第一遍少用一半时间。这样反复三轮以后,你对Socket的理解会扎实很多,因为你在自己动脑思考每一个阻塞和异常,而不是复制粘贴了事。
我个人的一个小习惯是写完代码之后一定会用一个简陋的日志函数把关键节点打印出来,比如“登录成功”“广播完成”“连接关闭”。这种方式比断点调试直观很多,尤其在网络程序中,日志几乎是唯一的“上帝视角”。很多难以排查的并发问题,全靠日志时间戳才能复盘出真实发生顺序。
到最后你会发现,这类带“主观题”三个字的题目,难的不是Java语法,也不是Socket API,而是你有没有一套稳定的设计思维。class怎么拆、接口怎么定、异常怎么收、并发怎么防,把这四件事想明白,任何换皮的网络编程题目落到你手里都是一道送分题。