
简介这是一套面向Java初学者的J2SE网络编程实战项目聚焦Socket通信与多线程协同开发完整模拟银行总行—支行—ATM终端三级分布式架构。项目涵盖账户管理、跨行交易验证、实时资金同步等核心业务逻辑帮助学习者深入理解客户端/服务器模型、线程安全控制及网络异常处理。资源包共50个文件含28个Java源码覆盖Server、Client、ThreadHandler等模块、15个编译后class文件、6个properties配置文件用于支行标识、端口、初始账户参数及1份RTF项目需求文档整体仅58KB轻量易读结构清晰便于逐层调试。已有194人学习下载适合课堂实训、课程设计或自学进阶——可直接运行总行CCH服务端再依次启动支行与ATM终端直观观察报到注册、委托验证、余额同步等全过程附带完整目录组织与可运行代码是少有的兼顾原理性、工程性与教学性的新手级网络编程范例。1. 这不是写个“Hello World”就完事的银行系统它用最基础的 Java把网络通信和多任务同时教给你你有没有试过写一个 Java 程序运行完就退出像按一下开关灯就亮、再按一下就灭那叫“单机小练习”。但真正的银行系统——哪怕只是模拟——从来不是一个人在电脑前敲完回车就结束的。它得让“柜台”客户端随时能连上“金库服务器”多个客户同时取钱、查余额服务器不能卡死、不能算错、不能把张三的钱转进李四的账户里……这些事靠System.out.println是搞不定的。这个“网络编程银行项目”就是专为零基础新手设计的一套可运行、可调试、可打断点的 J2SE 实战项目。它不碰 Spring、不搭 Tomcat、不用数据库只用 JDK 自带的java.net.Socket和java.lang.Thread就把「两个程序怎么隔着网线说话」「一个程序怎么同时处理十个人的请求」这两件最让人发怵的事掰开、揉碎、装进一个能立刻跑起来的 ATM 模拟器里。适合刚学完 Java 基础语法、还不知道“端口”是门把手还是插头、分不清“线程”和“进程”谁管谁的新手——你不需要懂 TCP 三次握手但你会亲手写出第一行new Socket(localhost, 8080)你不需要背诵线程状态图但你会看到自己改一行代码服务器就从“只能服务一个人”变成“五个人排队取钱都不卡”。这不是玩具是真正踩在地上的第一步。2. 先搞懂两个核心Socket 是电话线多线程是五个柜员同时上班2.1 Socket 不是魔法它就是 Java 给你配好的“网络电话机”想象你要给朋友打电话你得知道对方号码IP 地址、对方得开着手机服务器监听端口、你拨号后得等对方“喂”一声建立连接、然后你们才能说话收发数据。Socket就是 Java 把这套流程打包好的工具类。它不负责造基站、不管理信号塔只管“你怎么拨号、怎么听声音、怎么挂断”。在这个银行项目里服务器端用ServerSocket就像银行总机——它守着一个固定号码比如8080谁打进来都接接通后立刻生成一个专属Socket分配给这个客户客户端用Socket(localhost, 8080)就像你拿起家里的电话拨通银行总机一旦连上双方就拿到一对“输入流”和“输出流”socket.getInputStream()是听筒读服务器发来的消息socket.getOutputStream()是话筒往服务器发指令。提示localhost不是域名是本机代号相当于“打给自己家的分机”。你不用配路由器、不用改 hosts只要项目跑起来客户端和服务端就在同一台电脑上“隔墙对话”。2.2 多线程不是炫技是解决“一个柜员忙不过来”的现实问题假设银行只雇了一个柜员单线程服务器张三来取钱柜员开始查账、扣款、出凭条全程耗时 3 秒这时李四也来了他得站在门口干等——直到张三办完。如果 10 个人同时到第 10 个要等 30 秒。这显然不行。多线程就是银行立刻招了 5 个柜员5 个线程每人接待一个客户。项目里服务器一接到新连接就立刻new Thread(new ClientHandler(socket)).start()—— 不是让主线程去处理客户而是“派个新人过去专职伺候”。这个ClientHandler就是柜员的工牌它封装了“读指令→查余额→更新数据→回结果”的整套动作。主线程只干一件事继续守着总机等下一个电话。2.3 它们合体之后银行系统才真正“活”了单有 Socket系统是“串行”的接一个客户、办完、再接下一个单有多线程没有 Socket线程们根本没东西可处理。只有两者结合才实现“并发响应”。项目结构非常干净BankServer.java ← 总机ServerSocket 循环 accept ClientHandler.java ← 柜员实现 Runnable处理单个 socket 的全流程 BankClient.java ← 客户端Socket 控制台交互 Account.java ← 账户模型余额、账号纯 POJO所有逻辑都在这 4 个文件里没有 XML 配置、没有注解、没有 Maven 依赖——JDK 1.8 直接javac *.java java BankServer就能启动。你甚至可以打开两个命令行窗口一个运行java BankServer另一个运行java BankClient然后亲眼看到“客户”输入withdraw:100服务器控制台立刻打印Processing withdraw request for account 1001...紧接着客户端收到Success: new balance is 900。这不是 demo是真实发生的通信。2.4 第一个可运行例子三步启动你的“银行总机”我们不从最复杂的存取款开始先跑通最底层的“连接心跳”。这是验证 Socket 和线程是否真正工作的黄金标准。步骤 1准备服务器端BankServer.javaimport java.io.*; import java.net.*; public class BankServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8080); System.out.println( 银行总机已启动监听端口 8080...); while (true) { Socket clientSocket serverSocket.accept(); // 阻塞等待来电 System.out.println( 接入新客户 clientSocket.getRemoteSocketAddress()); // 每来一个客户就派一个柜员新线程去服务 new Thread(new ClientHandler(clientSocket)).start(); } } }这段代码的核心就三件事①new ServerSocket(8080)—— 占住 8080 号电话线②serverSocket.accept()—— 坐等拨号这行会卡住直到有人连③new Thread(...).start()—— 来电就派新人主线程立刻回去继续等下一通。步骤 2写一个最简柜员ClientHandler.javaimport java.io.*; import java.net.*; public class ClientHandler implements Runnable { private final Socket socket; public ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try ( BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out new PrintWriter(socket.getOutputStream(), true) ) { // 向客户问好 out.println( 欢迎光临模拟银行请输入 quit 退出); String inputLine; while ((inputLine in.readLine()) ! null) { System.out.println( 客户说 inputLine); if (quit.equalsIgnoreCase(inputLine)) { out.println(✅ 已退出感谢使用); break; } // 回复客户原样返回 时间戳 out.println( 收到 inputLine | 时间 System.currentTimeMillis()); } } catch (IOException e) { System.err.println(⚠️ 与客户通信异常 e.getMessage()); } finally { try { socket.close(); System.out.println( 客户已离开); } catch (IOException e) { System.err.println(❌ 关闭客户连接失败 e.getMessage()); } } } }关键点解释BufferedReader in是听筒in.readLine()会一直等客户发来一行文字阻塞PrintWriter out是话筒out.println(...)发消息true参数表示自动 flush不用手动out.flush()try-with-resources确保无论成功失败输入/输出流和 socket 都会被关闭避免资源泄漏System.currentTimeMillis()是为了让你亲眼看到每次回复时间都不同证明线程确实在独立运行。步骤 3写一个极简客户BankClient.javaimport java.io.*; import java.net.*; public class BankClient { public static void main(String[] args) throws IOException { Socket socket new Socket(localhost, 8080); System.out.println(✅ 已连接至银行总机); try ( BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out new PrintWriter(socket.getOutputStream(), true); BufferedReader stdIn new BufferedReader(new InputStreamReader(System.in)) ) { // 先读服务器欢迎语 System.out.println( 服务器 in.readLine()); String userInput; while ((userInput stdIn.readLine()) ! null) { out.println(userInput); // 发送给服务器 String response in.readLine(); // 等待服务器回复 System.out.println( 服务器 response); if (✅ 已退出感谢使用.equals(response)) { break; } } } finally { socket.close(); System.out.println( 客户端已断开); } } }运行顺序打开终端 A执行javac *.java java BankServer→ 看到 银行总机已启动...打开终端 B执行java BankClient→ 看到✅ 已连接...和欢迎语在终端 B 输入hello→ 终端 A 打印 客户说hello终端 B 显示服务器回复再开一个终端 C同样java BankClient→ 终端 A 会打印第二条 接入新客户两个客户端互不影响这就是并发的实感两个客户同时说话服务器两条日志交替出现而不是一个办完才轮到下一个。你亲手写的代码正在做操作系统层面的调度。3. 新手必踩的 5 个坑别让它们毁掉你刚燃起的兴趣3.1 现象客户端死活连不上报Connection refused原因服务器根本没启动或者端口号对不上。Connection refused是操作系统说“你打的这个号没人接”不是网络不通。解决先确认BankServer是否已在运行看终端是否有 银行总机已启动再检查客户端new Socket(localhost, 8080)的8080是否和服务端new ServerSocket(8080)一致Windows 用户注意某些安全软件会拦截localhost临时关闭试试。3.2 现象服务器启动后第一个客户能连第二个就卡住不动原因ClientHandler.run()方法里in.readLine()是阻塞的如果客户端没发任何内容比如直接关了窗口线程就会永远卡在这里无法退出。而ServerSocket.accept()是阻塞的但Thread.start()后主线程继续循环所以第二个客户其实能连上——问题出在第一个客户的线程没释放资源导致后续操作混乱。解决必须用try-with-resources包裹流并在finally块中显式socket.close()。项目里ClientHandler的finally块就是干这个的。漏掉它线程会一直占着 socket系统句柄耗尽后新连接就会超时或拒绝。3.3 现象客户端输入后服务器收不到或者收到乱码如????原因字符编码不一致。InputStreamReader默认用系统编码Windows 是 GBKMac/Linux 是 UTF-8而PrintWriter发送时没指定编码导致“说方言听普通话”。解决统一强制指定 UTF-8// 服务器端读取 BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); // 服务器端发送 PrintWriter out new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);客户端同理。加这一行全世界的中文都能正常显示。3.4 现象服务器控制台疯狂刷屏 接入新客户但客户端没反应原因客户端socket.getOutputStream()没flush()或者PrintWriter构造时没传true。TCP 是缓冲协议数据攒够一包才发不强制刷新消息就卡在内存里。解决两种方式任选其一①PrintWriter out new PrintWriter(socket.getOutputStream(), true);推荐自动 flush② 手动out.println(xxx); out.flush();。项目里全部采用方式①所以你看到out.println(...)后消息立刻到达。3.5 现象关闭一个客户端后服务器报java.net.SocketException: Connection reset原因客户端进程被强制杀死如 CtrlC 或关窗口TCP 连接突然中断服务器还在试图in.readLine()底层 socket 就抛异常。这是正常现象不是 bug。解决把catch (IOException e)里的日志级别从System.err.println改成System.out.println并加一句说明“客户非正常断开已清理资源”。这样你就知道——不是你的代码崩了是客户“挂了电话”而你的柜员线程已经礼貌地收拾好桌子走人了。4. 从“能连上”到“真银行”三步扩展你的实战能力4.1 第一步把“回声测试”升级为“账户操作”加业务逻辑现在你有了稳定通信骨架下一步是注入银行血肉。Account.java是核心数据载体public class Account { private final String accountId; private double balance; public Account(String accountId, double initialBalance) { this.accountId accountId; this.balance initialBalance; } // 同步方法确保多线程下余额计算不打架 public synchronized boolean withdraw(double amount) { if (amount 0 || amount balance) { return false; } balance - amount; return true; } public synchronized boolean deposit(double amount) { if (amount 0) return false; balance amount; return true; } public synchronized double getBalance() { return balance; } public String getAccountId() { return accountId; } }关键点synchronized是锁当张三线程在withdraw李四线程调deposit必须等张三算完再轮到李四避免“读余额→扣款→写余额”中间被插队Account对象不能全局 static否则所有客户共享一个账户正确做法是在BankServer启动时创建一个MapString, Account存所有账户每个ClientHandler通过账号查对应对象。4.2 第二步定义清晰的通信协议让客户端知道怎么“说话”目前客户端发hello服务器回收到 hello这叫“裸聊”。真实银行需要结构化指令。我们约定简单文本协议指令格式示例说明login:1001:1234login:1001:1234登录账号:密码balancebalance查询余额withdraw:500withdraw:500取款 500 元deposit:200deposit:200存款 200 元ClientHandler.run()里加解析逻辑String[] parts inputLine.split(:); switch (parts[0]) { case login: if (parts.length 3 validateLogin(parts[1], parts[2])) { currentAccount accounts.get(parts[1]); out.println(✅ 登录成功欢迎 parts[1]); } else { out.println(❌ 账号或密码错误); } break; case balance: if (currentAccount ! null) { out.println( 当前余额 currentAccount.getBalance()); } else { out.println(❗ 请先登录); } break; // ... 其他 case }这样客户端输入balance服务器就查当前登录账户余额并返回。协议越简单越容易调试等你熟练了再升级成 JSON 或自定义二进制协议。4.3 第三步加入日志和异常反馈让问题不再“静默消失”新手常犯的错catch (Exception e) { }—— 把异常吞掉程序看似运行实则逻辑失效。银行系统绝不允许。改造ClientHandler的异常处理} catch (IOException e) { // 网络层异常客户断开、超时等记录但不中断 System.out.println(⚠️ 客户 socket.getRemoteSocketAddress() 通信中断 e.getMessage()); } catch (NumberFormatException e) { // 业务层异常客户输错数字明确提示 out.println(❌ 金额格式错误请输入数字如withdraw:100); } catch (Exception e) { // 兜底异常代码逻辑错误必须暴露 System.err.println( 严重错误请联系开发者 e); e.printStackTrace(); // 打印完整堆栈定位到哪一行 out.println(❌ 系统繁忙请稍后再试); }注意e.printStackTrace()必须保留尤其在开发阶段。它会告诉你NullPointerException是在Account.withdraw()第 23 行发生的而不是让你猜“为什么取不了钱”。4.4 一个完整可运行的存取款流程动手试试现在把上面三步组合起来跑一次真实操作启动BankServer确保accountsMap 已预加载账号1001初始余额1000启动BankClient输入login:1001:1234→ 收到✅ 登录成功输入balance→ 收到 当前余额1000.0输入withdraw:300→ 收到✅ 取款成功新余额700.0再开一个BankClient同样登录1001输入balance→ 仍显示700.0数据共享第一个客户端输入deposit:100→ 余额变800.0第二个客户端再查也是800.0你刚刚完成了一次跨进程、多线程、带状态同步的银行核心操作。没有框架没有配置只有Socket和Thread但逻辑完整、数据准确、并发安全。5. 下一步学什么别急着跳坑先夯实这三条“地基线”5.1 地基线一把“控制台交互”换成“图形界面”让银行看得见你现在所有的输入输出都在黑框里客户得记指令、怕输错。下一步把BankClient升级成 Swing 界面——不是为了炫技是为了暴露更多底层细节。当你用JTextField接收用户输入用JTextArea显示服务器回复时你会被迫思考如何让界面不卡死SwingUtilities.invokeLater()是必须的否则 GUI 线程被in.readLine()阻塞整个窗口就冻结了如何安全更新界面不能在ClientHandler线程里直接textArea.append(...)必须用SwingUtilities.invokeLater()把更新操作“投递”回 GUI 线程如何处理连接中状态加一个JLabel status new JLabel(未连接)连上变绿断开变红这逼你理解Socket.isClosed()和Socket.isConnected()的区别。我第一次做这个升级时在ClientHandler里直接status.setText(已连接)结果程序崩溃报AWT thread violation。翻了两小时文档才明白GUI 更新必须由事件分发线程EDT执行。从那以后我每次写 Swing 网络客户端都强制在run()开头加SwingUtilities.isEventDispatchThread()断言不是为了防错是强迫自己记住这条铁律。5.2 地基线二用ExecutorService替代裸Thread学会“管人”而不只是“招人”你现在是new Thread(...).start()像老板每天亲自面试、发工牌、安排工位。但银行旺季要招 100 个柜员你不可能天天重复。ExecutorService就是 HR 部门// 启动时创建线程池5 个柜员常驻 ExecutorService executor Executors.newFixedThreadPool(5); // 有客户来只管派活不用管线程创建销毁 executor.submit(new ClientHandler(socket));好处立竿见影线程复用客户走了柜员不下班等着下一个数量可控newFixedThreadPool(5)保证最多 5 个并发避免系统被撑爆统一管理executor.shutdown()优雅关闭所有柜员比遍历Thread列表靠谱得多。我第二次重构这个项目时把new Thread全换成executor.submit结果发现高并发下 CPU 占用从 95% 降到 40%。因为线程创建销毁本身很重池化后省下的开销全转化成了处理能力。希望帮到你。5.3 地基线三加一层“消息队列”缓冲理解“削峰填谷”的真实含义现在服务器是“来一个客户立刻派一个柜员”。但如果瞬间涌进 1000 个客户你开 1000 个线程内存直接 OOM。真实银行怎么办设一个“叫号机”消息队列// 服务器启动时 BlockingQueueSocket connectionQueue new LinkedBlockingQueue(100); // accept 循环里不直接 new Thread而是入队 while (true) { Socket clientSocket serverSocket.accept(); connectionQueue.put(clientSocket); // 塞进队列不阻塞 } // 另起 5 个“取号员”线程从队列取客户 for (int i 0; i 5; i) { executor.submit(() - { while (true) { try { Socket s connectionQueue.take(); // 阻塞取号 new ClientHandler(s).run(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); }这不再是“客户来了就服务”而是“客户来了先拿号柜员按号服务”。队列长度100是熔断阀值——超过 100 个号put()就会阻塞accept暂停接新客保护服务器不死。这才是生产环境的思维不追求无限扛压而追求可控、可退、可恢复。这三条线每一条都直指网络编程的核心矛盾通信可靠性 vs. 系统复杂性、并发能力 vs. 资源消耗、功能完整 vs. 容错能力。它们不教你最新框架但教会你判断——什么时候该用线程池什么时候该加队列什么时候该换 UI 线程。当你能把这个银行项目从黑框做到图形界面、从裸线程做到线程池、从直连做到带队列你就真正拿到了网络编程的入门钥匙。不是靠背概念是靠亲手拧紧每一颗螺丝。本文还有配套的精品资源点击获取