资讯动态

Java网络编程入门:Socket通信与多线程打造银行实战项目

发布时间:2026/10/15 12:48:41 来源:尧图企业网站定制
简介一套以银行网络系统为背景的Java标准版实战项目面向刚接触网络编程与并发处理的初学者。项目通过总行、支行与自动取款机终端的三级结构完整演示套接字通信、多线程并发、跨行身份验证与账户金额同步等核心逻辑适合作课程设计或综合练习。压缩包共五十个文件其中Java源码二十八个、编译后的类文件十五个、配置文件六个、说明文档一个整体仅五十八千字节代码量适中目录清晰便于逐文件研读。已有193人浏览学习对新手具有一定参考价值也适合作为结课作业或面试展示项目。读者可借此理解网络分层交互、线程同步、账户验证与总额平衡机制还可参照扩展需求尝试实现行内转账、跨行转账等未完成功能是巩固Java基础、提升实战能力的良好素材。1. 先弄明白一件事这个银行项目到底是个什么东西很多学 Java 的朋友卡在同一个地方语法书翻了好几遍面向对象、集合、异常都眼熟但一说到“做个项目”脑子就一片空白——不知道代码该往哪个文件里写更不知道“能跑的东西”长什么样。这个网络编程银行项目就是专门用来打通这层窗户纸的它是 J2SE 阶段最适合新手跟练的实战项目用到的核心技术就两个——Socket 通信和多线程运用。你把它理解成“两台电脑之间通过端口互相传数据同时一台服务器要同时服务好多个客户端”的完整演练而银行只是借来当业务场景的壳让你觉得每一步都有意义而不是对着黑底白字的控制台发呆。它就是给你这样的零基础新手准备的你不需要会框架不需要懂数据库只要你敲过几十行 Java 代码、知道类和对象大概是怎么回事就可以开始。2. 动手前先搞懂两个核心概念Socket通信和多线程2.1 Socket 通信不是玄学就是两台电脑拿端口传话我第一次听到“Socket 通信”这几个字的时候脑子里浮现的是一根看不见的网线。后来某导师打了个比方我才彻底明白你和你朋友打电话你俩的手机号就是 IP 地址而手机里不同的 App 就像是不同的“端口”——你不可能用微信的通道去接 QQ 的消息对吧Socket 干的事情就是一台电脑上的程序客户端拨出电话另一台电脑上的程序服务端接起电话两边建立一条专用的数据通道然后互相发消息。这就是网络编程里最底层的通信方式也是网上银行、在线游戏、聊天软件的老祖宗。具体到 Java 里服务端要做的三件事很固定创建一个ServerSocket绑定一个端口然后调用accept()方法等待别人来连。accept()这个方法是阻塞的意思就是它会一直“傻等”在那里直到有客户端真的连过来它才继续往下走。客户端那边则更简单创建一个Socket对象填上服务端所在的 IP 和端口号就能发起连接请求。一旦连接建立成功两边就各自拿到一个输入流和一个输出流——注意输入输出是站在你当前这个程序的角度说的你从流里read就是接收对方的数据你往流里write就是发给对方。一个新手的动作要领先分清谁是服务端、谁是客户端。这个区分不是身份的贵贱而是启动顺序的先后。服务端必须先运行、先进入等待状态客户端后启动去“够”它。顺序一颠倒客户端就会抛ConnectException报错信息大概意思是“连接被拒”。这和你先给朋友打电话、结果他还没开机是一个道理。记住 IP 和端口这两个词它们是你配置网络通信时唯二需要关心的一对参数。2.2 多线程就是银行里的多个柜台窗口说到多线程我特别愿意用银行大厅来打比方因为这个项目本身就是银行场景比喻和代码能对上号。你去银行办业务如果整个大厅只有一个柜台窗口那所有人就都得排队前面那个人在窗口前磨叽十分钟后面的人就得干等十分钟。这就是单线程程序一个程序从头到尾跑一条线遇到某个耗时的操作比如等待网络数据整个程序就卡住了什么都干不了。多线程干的事情就是多开几个窗口。每个窗口是一个独立的线程各自接待各自的客户互不干扰。在 Java 里你写一个类实现Runnable接口然后把逻辑写进run()方法再用new Thread(对象).start()把它启动起来一个新线程就诞生了。线程和线程之间的执行顺序是并发的你无法精确预料谁先谁后但它们共享同一个进程里的内存空间——这句话既是多线程强大的地方也是新手最容易翻车的地方。回到银行场景你就懂了服务端程序要同时接待很多个客户端如果只有一个线程在跑accept()那这个线程一会儿忙连接、一会儿忙收数据、一会儿忙发数据其他客户端全得在后头等着。解决办法就是主线程只负责接待新客户一旦accept()接到一个客户端就立刻创建一个新线程把这个客户丢给新线程去服务主线程自己马上转回身继续等下一个客户。这样整个系统就能同时服务很多客户每个客户都感觉自己被“秒回”了。多线程本身不难难的是多个线程同时访问同一个数据。比如在银行项目里多个客户同时在同一个账户上存款、取款如果两个线程同时读到余额是 1000 元然后各自加上 500再写回去那结果到底是 1500 还是 2000这个经典问题叫作“线程安全问题”你在做入门项目时不需要彻底解决它但你得知道危险存在并且至少学会用 Java 的synchronized关键字给关键方法加把锁让同一时刻只有一个线程能进入那个方法。2.3 这两个概念合起来就是这个项目的骨架现在你把两个概念拼起来整个银行项目的架构图就在脑子里清晰成型了服务端程序像一个银行总行它启动后监听某个端口比如 8888。每个客户端程序就是一位储户储户通过 Socket 连上总行。总行每接到一个连接就开一个新窗口新线程去服务这位储户储户在这个窗口前可以办各种业务查询余额、存款、取款、转账——这些业务背后其实就是客户端往服务端发一段文字服务端接到后分析这段文字、执行对应操作、再把结果文字发回去。这就是整个项目的核心骨架。你不用着急去想“要不要学 Spring”“要不要学 Netty”在入门阶段J2SE 原生的ServerSocket、Socket、Thread这三个类足够撑起一个完整的项目。而银行只是让你有一个清晰的业务逻辑去做增删改查把账户信息放进内存里的一个Map把操作流程写成分支判断就已经是一个像模像样的小系统了。接下来我要带你亲手把第一个例子跑起来——先把一条消息从客户端发到服务端让数据真正在网络上走通。3. 第一小步写一个能跑的最小Socket通信例子3.1 服务端代码让程序变成一台听电话的座机我先给你一个最简单的服务端程序它只做一件事启动后监听 8888 端口等到有人连过来就把它发来的一句话打印出来然后回一句“收到”。你不用管银行不银行的先把这条路走通再说。代码如下import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class SimpleServer { public static void main(String[] args) throws IOException { // 步骤1创建服务端套接字并绑定到8888端口 ServerSocket serverSocket new ServerSocket(8888); System.out.println(服务端已启动等待客户端连接...); // 步骤2accept()会一直阻塞在这里直到有客户端连上来 Socket socket serverSocket.accept(); System.out.println(有客户端连进来了); // 步骤3读取客户端发来的数据 BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); String message reader.readLine(); System.out.println(客户端说 message); // 步骤4给客户端回一句话 PrintWriter writer new PrintWriter(socket.getOutputStream(), true); writer.println(我已经收到你的消息了); // 步骤5释放资源 reader.close(); writer.close(); socket.close(); serverSocket.close(); } }这个代码你要盯住的就三处。第一处是new ServerSocket(8888)这里 8888 是端口号你可以换成任意没有被占用的数字比如 10086但客户端和服务器必须保持一致——端口号就像门牌号写错了就找不着门。第二处是accept()这一行它就像一个座机铃声响起之前的沉默什么都不做就是在等待。第三处是BufferedReader那两行InputStreamReader把网络传来的字节流转成字符流BufferedReader再把它包成带缓冲的读取器这样就能用readLine()一次读一整行文本。为什么用PrintWriter回消息因为它有一个很有用的构造参数true意思是每次println之后自动刷新缓冲区保证消息立刻发出去不会憋在缓冲区里。3.2 客户端代码把那通电话拨出去服务端写完了现在写客户端。客户端的工作就是主动去连服务端的 IP 和端口连上之后发一句话再读取服务端的回话。注意如果你只在本机测试服务端 IP 写成127.0.0.1或者localhost都可以这是你自己电脑的循环地址表示“这台机器自己跟自己通信”。import java.io.*; import java.net.Socket; public class SimpleClient { public static void main(String[] args) throws IOException { // 步骤1连接到服务端。127.0.0.1是本机地址8888是服务端端口 Socket socket new Socket(127.0.0.1, 8888); System.out.println(已连接到服务端); // 步骤2向服务端发送一行文本 PrintWriter writer new PrintWriter(socket.getOutputStream(), true); writer.println(你好服务端我是第一个客户端。); // 步骤3读取服务端返回的消息 BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); String response reader.readLine(); System.out.println(服务端回复 response); // 步骤4关闭连接 writer.close(); reader.close(); socket.close(); } }这段代码里最容易踩坑的一个细节是writer.println和reader.readLine()的对应关系客户端用println发出去一行文字结尾带着换行符服务端用readLine()按行读取看到换行符才算读到一条完整消息。同理服务端回话也用println客户端也用readLine()。规则就是发的一方必须用带换行的写法收的一方必须按行读。有人嫌麻烦想把println换成print结果对方readLine()永远在等换行符程序就这么卡住了这是新手必遇的第一道坎。3.3 让两个程序跑起来看到第一条消息运行的时候有一个顺序问题很多人第一次都栽在这里。你先启动SimpleServer看到控制台打印出“服务端已启动等待客户端连接...”然后再启动SimpleClient。这时候你会看到两个控制台窗口互相输出消息一个说“客户端说你好服务端”另一个说“服务端回复我已经收到你的消息了”。这就完整跑通了一次网络通信你的代码里第一次真正发生了跨程序的数据传递。如果你没按顺序来先把客户端启动了会看到一个大大的红色报错Exception in thread main java.net.ConnectException: Connection refused。这个报错的意思是客户端想连接 8888 端口但那个端口上根本没有程序在监听——服务端还没起来。所以从第一次起你就要养成习惯先服务端、后客户端这个顺序不是建议是规定。还有一个特别让人崩溃的问题服务端和客户端都启动了但控制台半天没动静也不报错。这时候你就要检查是不是没有满足“一行对一行”的约定。我刚才说了readLine()必须等到换行符才返回。如果你的代码用了writer.print(你好)而没有带换行数据确实发出去了但对端就是读不到。排查思路永远从“是不是流没刷新”“是不是缺少换行符”这两个方向切入八成问题都出在这里。4. 升级成多线程版本让服务器同时服务好几十个“客户”4.1 从单线程到多线程每来一个人就开一个新窗口3.1 里的服务端有个严重缺陷它处理完一个客户端就退出而且如果第一个客户端迟迟不发消息第二个客户端连上来也只能排队等着因为accept()只被调用了一次之后整个程序就跑完了。真正能用的服务端必须解决两件事第一不能只接一个客户第二不能因为一个客户慢了、卡了就拖累其他人。解决办法就是循环加多线程——这正好把第 2 章的知识用上。import java.io.*; import java.net.*; public class MultiServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8888); System.out.println(服务端已启动等待客户端连接...); while (true) { // 主线程不停接客每接到一个就交给新线程 Socket socket serverSocket.accept(); System.out.println(新客户到来 socket.getInetAddress()); // 把连接交给线程处理后主线程立刻回头继续accept() Thread thread new Thread(new ClientHandler(socket)); thread.start(); } } } class ClientHandler implements Runnable { private Socket socket; public ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try { BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter writer new PrintWriter(socket.getOutputStream(), true); String message; while ((message reader.readLine()) ! null) { System.out.println(收到 message); writer.println(服务端已处理 message); } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }while (true)配合accept()的写法是这里最关键的思路转换之前accept()只调一次现在它被放进循环里每接一个客户端就返回一个Socket主线程立刻创建一个ClientHandler线程去服务这个连接然后自己再回到accept()等待下一个。这就是经典的“线程每客户”模型。ClientHandler实现了Runnable接口逻辑放在run()方法里——所有连进来的客户就都交给这个代码块去服务了。run()里有一个while ((message reader.readLine()) ! null)这里的意思是持续读取客户端发来的消息读到一行就处理一行直到客户端主动关闭连接此时readLine()返回null循环才会退出然后进入finally块关闭 socket。看这段代码时你需要注意一件事run()方法不能声明throws IOException因为它重写自Runnable接口所以内部必须自己用try-catch包住最后用finally关资源。4.2 银行项目里的基本业务流程开户、存款、取款、转账有了多线程框架现在可以往里面填银行逻辑了。为了让问题简单我用一个Account类来表示银行账户里面存两个东西账号和余额。所有账户放在服务端内存里用一个MapString, Account管理。客户端发送过来的每个命令都设计成一个字符串约定好了格式服务端才能解析。import java.util.concurrent.ConcurrentHashMap; class BankService { // 用线程安全的HashMap存储所有账户账号 - 账户对象 private ConcurrentHashMapString, Account accounts new ConcurrentHashMap(); // 开户账号已存在返回false否则创建并返回true public synchronized boolean openAccount(String accountId) { if (accounts.containsKey(accountId)) { return false; } accounts.put(accountId, new Account(accountId, 1000)); // 新户默认1000元 return true; } // 存款直接给余额加钱 public synchronized void deposit(String accountId, double amount) { Account acc accounts.get(accountId); if (acc ! null) { acc.balance amount; } } // 取款余额不够就返回false public synchronized boolean withdraw(String accountId, double amount) { Account acc accounts.get(accountId); if (acc null || acc.balance amount) { return false; } acc.balance - amount; return true; } // 查询余额 public double getBalance(String accountId) { Account acc accounts.get(accountId); return acc null ? -1 : acc.balance; } } class Account { String id; double balance; public Account(String id, double balance) { this.id id; this.balance balance; } }这里用了ConcurrentHashMap而不是普通的HashMap原因是多个线程同时操作同一个 Map 时普通 HashMap 可能因为并发写入把内部结构弄坏轻则数据错乱重则直接抛ConcurrentModificationException。ConcurrentHashMap是线程安全的版本Java 里现成提供了它你直接拿来用就好。我给每个业务方法加了synchronized关键字这也是新手用多线程写数据时最容易漏掉的一步不加同步两个线程同时给同一个账户存钱余额就少算了一笔。这个问题的根源我在 2.2 提过你现在真的遇到了。转账怎么实现很简单如果从 A 转给 B就先从 A 账户执行withdraw如果成功再往 B 账户执行deposit。这两步必须放在同一个synchronized方法里否则可能出现“钱从 A 扣了但还没加到 B 上这时另一个线程看到了中间状态”的问题。这些细节点记不住没关系理解“共享数据需要保护”是重点具体写法以后还会反复练。4.3 把协议定好客户端和服务端用“报文”说话程序之间交流不能靠猜得有一套双方都听得懂的“暗号”在编程里这叫作“协议”。我给你一套最简版协议设计客户端发过来一行文本格式是“业务类型 参数1 参数2”中间用空格隔开。具体的命令如下客户端发送含义服务端返回OPEN 6222001开户账号是 6222001SUCCESS或EXISTSDEPOSIT 6222001 500给 6222001 存 500 元SUCCESS或ACCOUNT_NOT_FOUNDWITHDRAW 6222001 300从 6222001 取 300 元SUCCESS或INSUFFICIENT_BALANCEBALANCE 6222001查询余额BALANCE 1200.0或ACCOUNT_NOT_FOUND服务端收到一行消息后用String[] parts line.split( )把命令拆开parts[0]是业务类型后面的元素是参数。每条命令对应一个分支每个分支执行完把结果用writer.println()发回去客户端再用readLine()接住。这就是一个最原始的“请求-响应”模型——你在网上银行提交的任何一个操作底层都是这样来回传文本。设计协议时经常有新手掉进一个坑取款和转账都需要先校验余额够不够有人就只调withdraw没校验返回值。结果余额不够时客户端那边莫名收到一个失败指令服务端这边却已经把数据改了。所以你要记住这条心法凡是可能失败的请求客户端和服务端都必须处理失败的那个返回结果不能只写成功的路。初学的时候觉得“反正我写的测试代码不会错”但正是在这种侥幸心态下多线程并发场景会给你上一课。5. 新手必踩的坑连接、线程和数据的血泪经验5.1 一启动客户端就报 Connection refused这是新手上路第一道坎几乎每个人都会撞一次。现象是客户端刚运行控制台立刻抛出一大段红字里面写着Connection refused: connect。原因十有八九不是代码写错了而是服务端还没启动或者服务端已经因异常退出了。我曾经见过有人把服务端和客户端写在同一个main方法里顺序执行结果服务端的accept()还没执行到客户端就先跑去连了照样报这个错。解决的办法很死板也很管用先单独运行服务端等它打印出“等待客户端连接”的字样再运行客户端。如果之前马上要跑之前那个 ServerSocket 被占用的情况请看下面的 5.2。5.2 启动服务端时报 Address already in use现象是服务端在new ServerSocket(8888)这行抛出BindException: Address already in use。原因有两个一是你上次运行服务端的窗口没有关掉端口还被那个旧进程占着二是你自己把端口设置成了某个系统程序正在用的端口。解决先把旧的控制台窗口全部关掉或者换一个端口试试看。这里补一个实用技巧在代码里ServerSocket构造成功后调用serverSocket.setReuseAddress(true)可以在快速重启时避免这个麻烦但我更建议你先从养成“运行前检查旧进程”的习惯开始。如果换了端口客户端里的端口号也要同步改否则又会回到 5.1 的报错。5.3 数据发过去了对方却一直在傻等现象是两边都正常连接但你发了一句话对端怎么都不打印出来好像是接收方卡死了一样。原因大概率是发送端用了writer.print(...)而不是writer.println(...)。readLine()是等到换行符才知道“一条完整消息结束了”你只发了字面内容没发换行符对方就永远等下去。另一个隐蔽的原因是你的发送端没有开启自动刷新new PrintWriter(stream)这个写法不会自动把缓冲区的数据推出去数据攒在缓冲区里出不去你以为发了其实还在内存里。解决用new PrintWriter(stream, true)第二个参数传true表示自动刷新。这些细节不踩一次是真的记不住但踩过一次之后你以后写任何网络程序都会下意识检查这两处。5.4 用了多线程余额却越算越不对现象是开十个客户端同时给同一个账户存 100 元按理说余额应该增加 1000 元但运行几次之后数据总是不对有时候甚至差二三百。原因就是多个线程同时执行了acc.balance amount这一行。这行代码在底层其实是三步操作读取余额、做加法、写回余额。两个线程同时读到 1000各自加上 100又各自写回 1100那最后就被后写的那个人覆盖了另一个线程的加法就丢了。解决就是给操作余额的方法加synchronized修饰——它像一个门卫同一时刻只允许一个线程进这个方法其他人都在门外等。这也是 4.2 里我为什么把每个方法都写成synchronized的原因不是装模作样是刚需。给方法加锁之后你再跑一遍同样的并发测试余额就稳稳的了。6. 下一步怎么学把这个项目迭代出更强的版本这个项目带给你最大的收获不是“会写银行”而是把三件事打通了Java 的网络编程建模、多线程并发下的共享数据处理、以及一套完整的请求响应协议设计。当你把上面这些代码全部跑通之后下一步有三个升级方向按难度递增排列你可以挑一个来练手。第一个方向是修复框架缺陷。现在每个客户开一个新线程如果来一万个客户你就得开一万个线程系统很快就扛不住了。这时候去学java.util.concurrent包里的线程池用ExecutorService替代new Thread()让固定数量的线程轮转处理所有客户端连接。你百度搜“Java 线程池 ExecutorService 教程”就能找到大量资料这是你从“能用”走向“能用得稳”的第一个台阶。第二个方向是让数据真正持久化。你把ConcurrentHashMap换成读写文件甚至用 JDBC 操作 MySQL客户端每次操作后数据都要落盘服务端重启后数据还在。这一步做下来你就能理解为什么真实项目要引入数据库了也能理解 ORM 框架解决了什么痛点。第三个方向是挑战更高难度的通信框架。当你对原生ServerSocket的繁琐感到不耐烦时去看看 Netty——一个封装了底层通信细节的高性能网络框架本质就是不让你手写那一堆BufferedReader和PrintWriter。但注意Netty 不是你现在的阶段该学的建议你至少把原生 Socket 通信练熟、练到能画出它的时序图再碰。从那以后我每次写网络程序都会强制自己先把规则写下来——谁先发、谁后发、消息的终止符是什么、失败怎么办然后把这张纸条贴在屏幕上。贴出来之后再去写代码真的能少踩一半的坑。说到底入门项目的目的不是让你变成一个多线程专家而是让你亲手把一条数据从一个程序送到另一个程序再把十几行业务逻辑跑通顺亲眼看到自己写的代码真的能让两个程序对话。这个过程一旦经历一次你对编程的恐惧就会消掉一大截。希望帮到你下一步就看你的了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑