简介:这份资源是面向计算机专业学生与Java初学者的一套银行排号系统完整项目资料,基于Java Socket网络通信与Java GUI图形界面开发,后端采用Oracle数据库,适合作为课程设计、毕业设计或Java综合练习的参考方案。压缩包整体约292.61MB,内含项目全套源码与配套文档,源码均经过测试校正,可百分百成功运行,方便读者直接部署与二次开发。项目围绕银行排队取号、叫号、窗口调度等核心业务展开,涵盖Socket多客户端通信、GUI界面交互与数据库持久化等关键知识点,能帮助读者理解C/S架构下网络编程与界面设计的整合思路。目前已有475人浏览学习,说明该方案在同类课程设计中具有一定参考价值。对于需要完成相关项目设计、希望获得可运行代码与文档支撑的读者,这份资料能提供从源码结构到实现逻辑的完整参照,便于快速上手并对照学习。
1. 银行排号系统这套 Java Socket + GUI 源码,到底能解决什么
很多人做课程设计时卡在同一个地方:业务逻辑想得明白,但一落到「多台机器怎么同时连一个服务端、窗口怎么实时刷新队列」就写不动了。这套基于 Java Socket + Java GUI 的银行排号系统,解决的正是这个场景——它用原生 Socket 做 C/S 通信,用 Swing/AWT 搭客户端界面,后端接 Oracle 存业务数据,把「取号、叫号、队列管理、窗口状态同步」这条链路完整跑通。适合正在做 Java 课程设计、想找一个能跑起来的 C/S 架构参考的人,也适合想补 Socket 网络编程实战的初学者。它不是什么高并发中间件,而是一份结构清晰、能本地复现的完整工程,源码加文档一起给,省掉你自己拼通信协议和界面布局的时间。
2. 拆开看架构:Socket 通信层和 GUI 层是怎么分工的
拿到一个 C/S 项目,先别急着跑,先看清楚它把职责切在哪。这套系统的核心就两条线:一条是客户端和服务端之间的 Socket 长连接,负责把「取号请求」「叫号指令」「队列变更」这些消息传过去;另一条是每个客户端自己的 GUI 线程,负责把收到的数据渲染成界面。理解这个分工,后面调 bug 才知道该往哪一层看。
2.1 通信协议:自定义消息格式而不是直接传对象
原生 Socket 传数据,绕不开一个问题:TCP 是字节流,没有消息边界。你write两次,对面可能一次read全收走,也可能分两次收。常见做法是自定义一个简单的应用层协议,用「长度 + 类型 + 内容」的格式把每条消息框起来。
这套源码里我一般会看到类似这样的消息封装:
// 消息封装:用固定分隔符切分,避免粘包 public class Message implements Serializable { private String type; // 消息类型:TAKE_NUM / CALL_NUM / QUERY_QUEUE private String content; // 业务数据,通常是 JSON 或分隔字符串 private long timestamp; // 时间戳,用于排序和去重 // 序列化后通过 ObjectOutputStream 发送 public byte[] toBytes() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); oos.flush(); return bos.toByteArray(); } }逻辑说明:这里用 Java 原生序列化把对象转成字节数组,服务端收到后再反序列化还原。参数上,type决定服务端走哪个分支处理,content装具体业务字段,timestamp在排查「消息乱序」时特别有用。要注意的是,如果客户端和服务端的类版本不一致,反序列化会抛InvalidClassException,所以实体类最好固定serialVersionUID。
2.2 服务端并发模型:一个连接一个线程
银行排号这种场景,并发量不大——几个窗口加一台取号机,撑死十几个连接。所以源码大概率用的是「每来一个连接就开一个线程」的模型,而不是 NIO 或线程池。这不是偷懒,是场景匹配。
// 服务端主循环:accept 后交给独立线程处理 ServerSocket serverSocket = new ServerSocket(8888); System.out.println("排号服务已启动,监听 8888"); while (true) { Socket socket = serverSocket.accept(); // 阻塞等待客户端连接 // 每个客户端一个线程,互不阻塞 new Thread(new ClientHandler(socket)).start(); }逻辑说明:accept()会阻塞,直到有客户端连上来。拿到Socket后立刻丢给ClientHandler线程,主线程回去继续等下一个连接。参数上,端口号 8888 是常见选择,改端口要同步改客户端。这里有个血泪经验:如果ClientHandler里读流用了readObject(),而客户端发的是裸字符串,会直接卡死或抛异常,所以两端协议必须严格对齐。
2.3 GUI 层:Swing 事件线程不能碰网络 IO
Swing 有个铁律:所有界面更新必须在事件分发线程(EDT)里做,而网络 IO 是阻塞的,绝不能放在 EDT 里。这套源码如果写得规范,应该是「网络收数据 → 丢到队列 → EDT 定时取 → 刷新界面」这个套路。
// 客户端:后台线程收消息,通过 SwingUtilities 切回 EDT 更新界面 new Thread(() -> { try { ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); while (true) { Message msg = (Message) in.readObject(); // 关键:用 invokeLater 把界面更新切回 EDT SwingUtilities.invokeLater(() -> { if ("QUEUE_UPDATE".equals(msg.getType())) { queueLabel.setText(msg.getContent()); } }); } } catch (Exception e) { e.printStackTrace(); } }).start();逻辑说明:后台线程负责阻塞读,读到消息后用SwingUtilities.invokeLater把更新动作排进 EDT 队列。参数上,msg.getType()决定更新哪个控件。如果直接把queueLabel.setText()写在后台线程里,界面会偶发不刷新甚至卡死,这是新手最容易翻车的地方。
3. 本地跑起来:Oracle 建库、导源码、改配置的完整步骤
架构看懂了,接下来是让它真的跑起来。这套系统依赖 Oracle,所以环境准备比纯 Java 项目多一步。下面按「装环境 → 建库 → 导工程 → 改配置 → 启动」的顺序走,每一步都给出可抄的命令和参数。
3.1 环境准备:JDK、Oracle、IDE 三件套
先确认版本,版本不对后面全是玄学问题。常见做法是 JDK 8 配 Oracle 11g 或 19c,因为老项目的 JDBC 驱动和语法对高版本 JDK 兼容性一般。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 源码大概率用 8 编译,高版本可能有模块化报错 |
| Oracle | 11g / 19c | 19c 需注意驱动包版本 |
| JDBC 驱动 | ojdbc8.jar | 放项目 lib 目录并加入 Build Path |
| IDE | Eclipse / IntelliJ IDEA | 导入普通 Java 工程即可 |
检查 JDK 是否装好:
java -version # 期望输出:java version "1.8.0_xxx" javac -version如果java -version报「不是内部或外部命令」,说明环境变量没配,需要把JAVA_HOME指向 JDK 安装目录,并把%JAVA_HOME%\bin加进Path。
3.2 建库建表:用 SQL 脚本还原数据结构
源码文档里一般会带.sql文件,直接拿去执行。如果没有,就按业务字段自己建。核心表通常三张:号码表、窗口表、叫号记录表。
-- 创建表空间用户(19c 常见做法) CREATE USER bank_queue IDENTIFIED BY bank123; GRANT CONNECT, RESOURCE TO bank_queue; -- 号码表:记录每个取号请求 CREATE TABLE t_queue ( id NUMBER PRIMARY KEY, num VARCHAR2(10), -- 号码,如 A001 status NUMBER(1), -- 0 等待 1 办理中 2 已完成 create_time DATE DEFAULT SYSDATE ); -- 窗口表:记录窗口当前叫到的号 CREATE TABLE t_window ( win_id NUMBER PRIMARY KEY, win_name VARCHAR2(20), curr_num VARCHAR2(10) );逻辑说明:t_queue的status字段是队列状态机的核心,取号时插 0,叫号时改 1,办完改 2。t_window存每个窗口当前处理的号码,客户端刷新时查这张表。参数上,num用VARCHAR2而不是数字,因为要带字母前缀。注意 Oracle 里DATE类型默认带时分秒,如果只要日期得用TRUNC。
3.3 导入工程并改数据库连接
把源码导入 IDE 后,第一件事是找数据库配置文件。老项目常见两种写法:一种是硬编码在DBUtil.java里,一种是放在db.properties。
// DBUtil.java 常见写法 public class DBUtil { private static final String URL = "jdbc:oracle:thin:@localhost:1521:orcl"; // 19c 可能是 /orclpdb private static final String USER = "bank_queue"; private static final String PWD = "bank123"; public static Connection getConn() throws Exception { Class.forName("oracle.jdbc.driver.OracleDriver"); return DriverManager.getConnection(URL, USER, PWD); } }逻辑说明:URL里的localhost:1521:orcl是 SID 写法,19c 多用服务名写法localhost:1521/orclpdb,写错会报ORA-12505。Class.forName加载驱动,JDK 8 下必须写,否则报No suitable driver。参数上,用户名密码要和建库时一致。
3.4 启动顺序:先服务端后客户端
顺序错了会连不上,这是最常见的翻车点。
- 先运行服务端主类(通常叫
ServerMain或BankServer),看到「监听 8888」才算起来。 - 再运行客户端主类(
ClientMain或LoginFrame),输入服务端 IP 和端口。 - 多开几个客户端模拟多个窗口,验证队列是否同步。
# 命令行启动示例(假设已编译到 out 目录) java -cp "out;lib/ojdbc8.jar" com.bank.server.ServerMain java -cp "out;lib/ojdbc8.jar" com.bank.client.ClientMain逻辑说明:-cp指定类路径,Windows 用分号分隔,Linux/Mac 用冒号。lib/ojdbc8.jar必须带上,否则连不上 Oracle。如果客户端报Connection refused,先确认服务端是否真的在监听,用netstat -ano | findstr 8888查端口。
4. 避坑排查:这套 C/S 项目最容易翻车的五个地方
跑通只是第一步,真正花时间的是排错。下面这五条是我拆这类项目时反复遇到的,每条都按「现象 → 原因 → 解决」写清楚,照着查能省不少时间。
4.1 现象:客户端连不上,报 Connection refused
原因:服务端没启动,或者端口被占用,或者客户端填的 IP 不对。银行排号系统如果服务端和客户端不在同一台机器,客户端不能填localhost。
解决:先在服务端机器上netstat -ano | findstr 8888确认端口在监听。如果被占用,改服务端端口并同步改客户端。跨机测试时,客户端填服务端的局域网 IP,并确认防火墙放行该端口。
4.2 现象:界面卡死,点按钮没反应
原因:网络 IO 或数据库查询写在了 EDT 里。Swing 的事件线程被阻塞,界面自然不刷新。
解决:把所有socket.getInputStream().read()和 JDBC 查询放到独立线程,界面更新用SwingUtilities.invokeLater切回 EDT。检查代码里有没有在按钮的actionPerformed里直接做耗时操作。
4.3 现象:Oracle 报 ORA-12505 或 ORA-28001
原因:ORA-12505是连接串里的 SID/服务名写错,ORA-28001是密码过期。Oracle 默认密码有效期 180 天,老项目放久了就会撞上。
解决:ORA-12505用lsnrctl status看监听器注册的服务名,改成正确写法。ORA-28001用ALTER USER bank_queue IDENTIFIED BY bank123;重置密码,或者直接改用户为不过期。
4.4 现象:反序列化报 InvalidClassException
原因:客户端和服务端的实体类serialVersionUID不一致,或者类结构改了没同步。
解决:给所有要序列化的类显式声明private static final long serialVersionUID = 1L;,两端保持一致。改类结构后重新编译两端,别只编译一边。
4.5 现象:号码重复或队列错乱
原因:号码生成用了「查最大值 +1」但没加锁,多个客户端同时取号会撞号。或者队列刷新有延迟,界面显示的是旧数据。
解决:号码生成放到服务端单点做,用数据库序列或synchronized保证原子性。客户端刷新用定时器轮询或服务端主动推送,别依赖手动刷新。如果用了序列,注意 Oracle 序列的CACHE参数,缓存大了重启会跳号。
5. 进阶玩法:把轮询改成推送,再补一层数据一致性校验
基础版跑通后,如果想让它更像回事,有两个方向值得动手:一是把客户端的定时轮询改成服务端主动推送,二是给队列操作加一层校验,防止并发下的状态错乱。这两点做完,这套课程设计就不只是「能跑」,而是「经得起问」。
5.1 从轮询到推送:减少无效请求
基础版客户端大概率是每隔几秒查一次数据库。连接一多,数据库压力就上来了。改成推送的思路是:服务端维护一个在线客户端列表,任何队列变更都广播给所有客户端。
// 服务端:维护在线客户端,变更时广播 private static final List<ClientHandler> ONLINE = new CopyOnWriteArrayList<>(); public static void broadcast(Message msg) { for (ClientHandler handler : ONLINE) { try { handler.send(msg); // 内部用 ObjectOutputStream 写回 } catch (Exception e) { ONLINE.remove(handler); // 发送失败说明连接断了,移除 } } }逻辑说明:CopyOnWriteArrayList在遍历时安全,适合读多写少的在线列表。broadcast在每次叫号、取号后被调用。参数上,send方法内部要加同步,因为多个线程可能同时往同一个ObjectOutputStream写,不加锁会串包。这一步做完,客户端就不需要轮询了,界面响应也更快。
5.2 数据一致性:叫号操作要防重复
多个窗口同时点「叫号」,如果不加控制,可能叫到同一个号。常见做法是在数据库层用条件更新,靠UPDATE的返回行数判断是否抢到。
-- 原子叫号:只有 status=0 的号才能被叫走 UPDATE t_queue SET status = 1 WHERE id = ( SELECT id FROM ( SELECT id FROM t_queue WHERE status = 0 ORDER BY create_time ) WHERE ROWNUM = 1 ) AND status = 0;逻辑说明:子查询取出最早等待的号,外层UPDATE带AND status = 0条件。如果两个窗口同时执行,只有一个能更新成功(返回 1 行),另一个返回 0 行,据此判断是否叫号成功。参数上,ROWNUM = 1保证只取一个,ORDER BY create_time保证先来先服务。这是 Oracle 里实现「乐观锁」的常见写法,比在 Java 层加锁更可靠。
5.3 验证方法:多客户端并发压一遍
改完之后怎么确认没问题?开五个客户端,同时疯狂点取号和叫号,观察三件事:号码有没有重复、队列顺序对不对、界面有没有卡死。如果号码重复,回去查UPDATE的返回行数判断逻辑;如果界面卡死,回去查 EDT 那条线;如果顺序乱,查create_time的精度,OracleDATE只到秒,高并发下同秒的号排序会不稳定,可以换成TIMESTAMP。
从那以后我每次拆这类 C/S 项目,都强制先跑一遍「多客户端并发」再去看代码,因为单机跑通和并发跑通完全是两回事。希望帮到你。
本文还有配套的精品资源,点击获取