
简介一套完整的银行排号系统设计与实现项目基于Java Socket完成客户端与服务器端的网络通信并利用Java GUI构建人机交互界面数据存取搭配Oracle数据库功能覆盖取号、叫号、窗口调度与排队状态查看等典型应用场景。项目主要面向计算机专业高校学生、课程设计或毕业设计需求者以及希望将Java网络编程知识落地的进阶开发者。压缩包整体大小约292.61MB内含项目全套源码与完整设计文档源码均经过测试校正可百分百运行文档对系统架构、模块划分和关键实现进行了说明便于读者快速梳理排号逻辑与通信流程。目前已有475人浏览学习适合作为相关项目设计的参考模板也可用于学习Socket通信、多线程并发、GUI事件处理和数据库交互等实用技能是理论与实践结合的高质量参考资源。1. 拿到这份基于 Java Socket Java GUI 的银行排号系统源码我最先确认的是它到底能不能跑和不少课程设计卖家给的半成品不一样这份银行排号系统压缩包里不是单个 Java 文件而是一整套 C/S 结构的完整工程客户端是 Java GUI 写的叫号界面服务端用 Socket 监听请求数据落进 Oracle。拆开后我发现它的价值不只是「能交差」——如果你正在学 Java 网络编程或者做毕业设计想选一个能讲清多线程、GUI 事件模型、数据库事务的项目这套代码比空谈理论有用得多。当时我先跑起来看了一圈确认了它能解决的真实痛点大厅取号、柜员叫号、窗口状态变更、等待列表刷新这些业务闭环都在。下面不吹资源按我排障的顺序从架构、核心代码、GUI 客户端的线程处理最后落到实际运行遇到的高频问题全部说一遍。2. 架构与数据库设计为什么这套排号系统要选 C/S 和 Oracle2.1 为什么用 Java Socket 做通信层而不是 RMI 或 HTTP银行排号业务本质上是内网低并发的实时通信场景。取号终端、柜员叫号端和综合管理端都需要实时知道当前排队号码状态。如果走 HTTP要么加轮询要么引入 WebSocket 或消息推送组件这对一个课程设计或中小型实训项目来说复杂度偏高。RMI 虽然也是 Java 原生的但需要额外维护 Registry 服务对象序列化耦合严重换一台机器跑容易出问题。所以这套系统选择 Java Socket 是合理的。服务端建立一个 ServerSocket客户端主动连上来之后保持长连接所有指令通过自定义文本协议传输。这个方案的好处有三个第一不依赖第三方容器部署简单第二可以很直观地展示 TCP 连接、多线程通信、流读写和面试时被追问的「Socket 网络编程」知识直接对应第三服务端能精确控制每个客户端连接的状态比如柜员在哪个窗口、当前叫到哪个号。实际项目里端口号一般会选一个 8000 到 9999 之间的空闲端口比如 9001。如果和本机其他服务冲突启动日志直接会报BindException这个问题我在后面的避坑章节里详细说。2.2 排号系统数据库表设计号码状态与窗口业务的映射关系Oracle 在这套系统里负责两件事一是持久化所有排队记录防止服务端重启后号码乱掉二是记录叫号流水方便后面统计业务量。我从源码文档里整理出的最简表结构大概有 5 张核心表表名作用关键字段T_QUEUE排队主表ID、BIZ_CODE、WINDOW_ID、STATUS、CREATE_TIME、CALL_TIME、FINISH_TIMET_WINDOW窗口表WINDOW_ID、WINDOW_NAME、WORKER_NAME、STATUST_BIZ_TYPE业务类型表BIZ_TYPE_ID、TYPE_NAME、PREFIX_CHART_CALL_LOG叫号流水表LOG_ID、QUEUE_ID、WINDOW_ID、ACTION、ACTION_TIMET_OPERATOR操作员表OP_ID、OP_NAME、PASSWORD、ROLE其中T_QUEUE.STATUS是最关键的状态字段。通常维护这几个值WAITING已取号等待叫号CALLED已被柜员叫号FINISHED业务办理完成CANCELED过号或取消为什么状态用数字或字符串而不是直接删记录因为叫号、完成、过号都要在日志里留痕。如果直接删除统计就会失真。这也是设计上值得学习的一点排队记录做追加不做原地覆盖。Oracle 里建主键或者生成号码时我建议用序列SEQ_QUEUE_ID尽管项目里也有人用MAX(ID)1但并发时容易重复后面避坑章节会展开。2.3 Socket 应用层报文协议让两类客户端对接同一套服务端客户端和服务端之间不能只传「一句话」就完事因为取号请求、叫号请求、状态查询请求格式不同。源码里用的是一种带命令前缀的文本协议我用伪代码还原一下命令格式CMD,参数1,参数2,参数3\n CMD 取值TAKE_NUMBER / CALL_NEXT / FINISH / QUERY_STATUS / HEARTBEAT服务端用一个BufferedReader.readLine()读取客户端发过来的这一行然后按逗号拆分匹配命令字符串。这个设计很简单比 Java 对象序列化更稳定而且可以直接用 telnet 或者写一段 Socket 测试脚本手工验证不用依赖整个 GUI 界面。如果在真实项目里我一般会加上一个协议版本号比如改成V1,CMD,参数这样以后服务端升级还能兼容旧客户端。不过作为演示项目直接用CMD开头也够用。3. 服务端核心实现多线程 Socket 服务、叫号逻辑与 Oracle 落库3.1 服务端启动入口与业务线程池服务端的主流程是这样启动时加载 JDBC 配置创建线程池然后进入 accept 循环每接受到一个新 Socket 连接就交个一个独立的业务线程去处理。这样能保证多个客户端同时取号时每个客户端不会互相堵住。下面是我还原的服务端核心代码骨架// BankServer.java import java.io.*; import java.net.*; import java.util.concurrent.*; public class BankServer { private static final int PORT 9001; // 核心线程 8 个最大线程 20 个队列长度 200 private static final ExecutorService POOL new ThreadPoolExecutor(8, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200)); public static void main(String[] args) throws IOException { ServerSocket server new ServerSocket(PORT); System.out.println(银行排号服务端已启动端口 PORT); while (true) { Socket socket server.accept(); POOL.execute(new ClientHandler(socket)); } } }这段代码是整个服务端的入口。new ServerSocket(PORT)会在指定端口开启监听accept()会阻塞等待客户端连接。ThreadPoolExecutor的参数值得说明一下核心线程 8 个适用于一般的毕设和实训环境如果你们学校机房有 30 个以上终端建议把核心线程提高到 16 到 32队列长度也同步调大。如果核心线程和最大线程都设成 1那就相当于串行处理第一个终端取号时第二个终端会一直等待。实际运行中BankServer.java应该常驻后台运行。很多新手直接在 IDE 里点一次运行关掉控制台再点第二次就会报端口被占用。这一点后面我会单独说。3.2 取号与叫号逻辑状态机是排号系统的核心每一个ClientHandler负责处理一个客户端连接。客户端发来TAKE_NUMBER,业务类型之后服务端要做三件事生成不重复的排队号码、写入数据库、把号码返回给客户端。取号的核心逻辑我用代码片段说明// ClientHandler.java 内部简化版 public void takeNumber(String bizType) throws Exception { Connection conn DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); conn.setAutoCommit(false); // 通过序列生成当日单号避免并发重复 PreparedStatement ps conn.prepareStatement( SELECT SEQ_QUEUE_ID.NEXTVAL FROM DUAL); ResultSet rs ps.executeQuery(); long seqId 0; if (rs.next()) { seqId rs.getLong(1); } String prefix getBizPrefix(bizType); String queueNo String.format(%s%03d, prefix, seqId % 1000); PreparedStatement insert conn.prepareStatement( INSERT INTO T_QUEUE (ID, BIZ_CODE, QUEUE_NO, STATUS, CREATE_TIME) VALUES (?, ?, ?, WAITING, SYSDATE)); insert.setLong(1, seqId); insert.setString(2, bizType); insert.setString(3, queueNo); insert.executeUpdate(); conn.commit(); // 返回号码给客户端 out.write(TAKE_OK, queueNo \n); out.flush(); }这里最关键的是不要用SELECT MAX(ID)1来生成号。多线程同时取号时两个线程都查到同一个最大 ID然后都插入相同号码直接主键冲突。用 Oracle 序列可以保证并发环境下的唯一性。seqId % 1000只是演示取三位数如果一天业务量超过一千笔可以改成% 10000取四位。叫号逻辑稍微复杂一点。柜员点「呼叫下一个」时服务端要把当前窗口关联到队列中最早WAITING的那条记录并把状态改成CALLED同时记录叫号时间。SQL 大致是这样SELECT ID FROM ( SELECT ID FROM T_QUEUE WHERE STATUS WAITING ORDER BY ID ASC ) WHERE ROWNUM 1拿到这个 ID 之后再用UPDATE更新状态和窗口号。为什么不能直接用第一个语句就 update因为在多线程叫号时两个窗口可能同时抢到同一条等待记录。所以稳妥的做法是把「取下一个号」和「更新状态」放在同一个事务里用SELECT ... FOR UPDATE SKIP LOCKED锁定记录。这个写法 Oracle 11g 以后都支持是高并发下比较可控的取数方式。3.3 JDBC 连接与事务叫号时不能把失败写在客户脸上服务端操作数据库的常见坑就是连接不释放和乱提交事务。我复查源码时发现这个项目在ClientHandler里使用try-with-resources管理 Connection 和 Statement这个习惯很好。我建议库表操作遵循以下纪律取号、叫号、完成这三个写操作都显式开启事务setAutoCommit(false)所有的查询、更新、插入完成之后手动commit()任何异常都要rollback()避免出现「界面显示叫号成功数据库里状态还是 WAITING」的情况关闭连接放到finally块或者直接使用 Java 7 的 try-with-resources 语法代码层面最典型的写法是这样try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); ps.executeUpdate(); conn.commit(); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw e; }很多新手只写conn.close()不写 rollback一旦 update 执行一半报错数据库状态就是错乱的。虽然本项目规模小这个习惯仍然值得保留因为后续扩展「过号」「预约」「VIP 优先」时事务范围会越来越大。4. Java GUI 客户端Swing 界面与 Socket 通信多线程踩坑4.1 用 Swing 而不是 AWT 搭界面Java GUI 的课程设计中AWT 是教科书常提的老技术但真正做项目我更推荐 Swing。Swing 的组件更丰富、外观跨平台一致而且JFrame、JButton、JTable这些组件用起来比 AWT 顺手得多。这套排号系统的客户端起两个角色大厅取号机用户和柜员操作系统。大厅取号机界面相对简单就是几个大按钮和显示当前号码的面板柜员端则需要显示等待列表、每个窗口状态、叫号操作按钮。下面是一个简化版的取号界面创建代码// TakeNumberFrame.java 片段 JFrame frame new JFrame(银行取号机); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400, 300); JPanel panel new JPanel(new BorderLayout()); JLabel numberLabel new JLabel(请点击按钮取号, SwingConstants.CENTER); numberLabel.setFont(new Font(宋体, Font.BOLD, 32)); JButton takeBtn new JButton(取号); panel.add(numberLabel, BorderLayout.CENTER); panel.add(takeBtn, BorderLayout.SOUTH); // 点击取号时不能直接在这里写 Socket 阻塞逻辑 takeBtn.addActionListener(e - { new TakeNumberWorker(numberLabel).execute(); });注意takeBtn.addActionListener里的写法。ActionListener运行在 Swing 的 EDTEvent Dispatch Thread事件派发线程里如果你在这里直接socket.getInputStream().read()界面就会卡住。很多刚学 Java GUI 的人会在这里翻车点完按钮界面转圈窗口拖不动最后只能强制杀进程。正确做法是使用SwingWorker或单独创建线程网络请求拿到结果后再回到 EDT 更新界面。4.2 读取 Socket 的线程必须和事件线程分离客户端同时要维护一条到服务端的 TCP 连接并持续读取服务端推送过来的状态变化比如「当前叫到 A012请到 3 号窗口」。这个读取动作绝不能放在事件线程里做。我常用的做法是启动一个单独的SocketListener线程在后台阻塞循环读服务端消息然后用SwingUtilities.invokeLater把更新动作切回事件线程// SocketListener.java 片段 new Thread(() - { try (Socket socket new Socket(serverIp, port); BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream(), UTF-8)); PrintWriter out new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), UTF-8), true)) { while (true) { String line in.readLine(); if (line null) break; final String msg line; SwingUtilities.invokeLater(() - { // 更新界面上的当前叫号标签 numberLabel.setText(msg); }); } } catch (IOException e) { SwingUtilities.invokeLater(() - numberLabel.setText(服务端连接断开)); } }).start();invokeLater的本质是让更新 UI 的代码在事件派发线程上排队执行避免在后台线程直接改组件引发线程安全问题。这里的网络流两边都指定了UTF-8这是避免中文乱码非常关键的一句话源码里如果少写了这个参数换个环境运行就可能出现A0??或者方框乱码。4.3 客户端心跳与断线重连服务端重启后已经建立连接的老客户端不会自动感知断开。Socket 读取时readLine()会返回null但这是阻塞状态下比较迟钝的反映。更好的方案是客户端周期性发送心跳包。这里的心跳包不是必须的但加上会让项目完整度更高// 每 5 秒发送一次心跳 Timer timer new Timer(5000, e - { if (out ! null) { out.write(HEARTBEAT\n); out.flush(); } }); timer.start();HEARTBEAT不是业务请求服务端收到后只需要回一个PONG或者直接忽略。它的意义有两个方面一是维持连接不被中间网络设备意外回收二是让服务端能清理已经失效的客户端连接。排号系统如果挂在大厅的触屏一体机上断线重连必须能在不重启界面的情况下自动恢复否则大堂经理得天天找人重启应用。5. 排查避坑端口冲突、Oracle 驱动与 GUI 卡死的高频原因这一章是排号系统部署过程中最容易踩实的坑我在复现源码时把几个典型问题都整理出来了。5.1 服务端重复启动报端口被占用现象运行BankServer.main时报java.net.BindException: Address already in use: JVM_Bind。原因上一个服务端进程没有退出常见情况是之前直接从 IDE 点红色停止但后台线程没有完全释放或者端口被其他程序占用。解决先确认端口占用进程再结束进程或改端口。# Windows 下查看 9001 端口占用 netstat -ano | findstr 9001 # 最后一列是 PID结束进程 taskkill /PID 1234 /F如果是在同一台机子上反复调试我一般直接把端口改成 9101让服务端和客户端读取同一个配置文件。5.2 连接 Oracle 时报 ClassNotFoundException现象启动取号或叫号功能时控制台出现java.lang.ClassNotFoundException: oracle.jdbc.OracleDriver。原因项目引用了ojdbc包但运行时 classpath 里没有这个 jar。特别是用 Tomcat 或者轻量级 IDE 跑的时候容易漏配。解决确认ojdbc8.jar在项目的 lib 目录中并且在 IDE 里把它添加为 Library。命令行运行的话这样写java -cp .;lib\ojdbc8.jar;lib\其他依赖.jar BankServerOracle 12c/19c 建议用ojdbc8而不是老的ojdbc6对 JDK 8 以上兼容性更好。5.3 GUI 点按钮后窗口卡死现象取号机界面上点「取号」按钮按钮按下去没反应整个窗口变成「未响应」状态。原因ActionListener里直接执行了 Socket 读写或Thread.sleep()把 Swing 事件派发线程堵住了。事件线程一堵所有按钮、标签都没法重绘窗口就卡死了。解决用SwingWorker或独立线程做网络操作。以取号为例new SwingWorkerString, Void() { Override protected String doInBackground() throws Exception { // 这里执行 socket 发送与接收 return sendCommand(TAKE_NUMBER,A); } Override protected void done() { try { numberLabel.setText(get()); } catch (Exception e) { numberLabel.setText(取号失败); } } }.execute();doInBackground是在后台线程执行的done会回到 EDT 执行所有界面更新操作都必须放在done方法里这样才符合 Swing 线程模型。5.4 中文乱码或消息错位现象取号成功后客户端显示A001?或者?号服务端日志里显示的中文业务名变成乱码。原因SocketInputStream和OutputStream两头编码不一致。Windows 控制台默认 GBK代码里如果没指定编码就会使用系统默认编码传过去的中文字节串在不同机器上解析出不同字符。解决在 Socket 流创建时显式指定编码。读取用new InputStreamReader(socket.getInputStream(), UTF-8)写出用new OutputStreamWriter(socket.getOutputStream(), UTF-8)两头统一成 UTF-8。数据库表字段也建议使用 NVARCHAR2 或 VARCHAR2 配合 AL32UTF8 字符集避免 Oracle 客户端和服务端字符集转换产生问号。5.5 并发取号时号码重复或插入报主键冲突现象两个取号终端同时按取号一个拿到A001另一个也拿到A001数据库报ORA-00001: unique constraint violated。原因号码生成逻辑用了SELECT MAX(ID)1两个事务同时执行时查到同一个最大值。前面我写的 Oracle 序列NEXTVAL就是为规避这个问题但许多课程设计源码为了简化会手写这个查询。解决把号码生成改成数据库序列或并发锁。如果不想新增序列可以在服务端加一个单例的号码生成器private static synchronized String getNextQueueNo(String prefix) { // synchronized 保证并发环境下一个线程只生成一个号 String queueNo prefix String.format(%03d, counter); return queueNo; }这是代码层面的兜底能保证同一 JVM 内不重复。但最稳妥的方案仍是依赖 Oracle 序列。每次生成号码后立即插入然后才释放锁。6. 进阶压测服务端、参数调优以及把排号记录导出为报表正常跑通这套系统只是第一步我拆资源时还会做两件额外验证第一是写一个多小时的压力测试确认服务端在并发取号时不把号码发重第二是把数据库里的排号流水导出成业务报表让项目答辩时有数据可讲。压测方法很简单不需要引入 JMeter写一个 Java Socket 客户端脚本模拟 50 个线程同时取号看最终数据库里有没有重复号码// 压测入口大家可以根据自己机器调整并发数 public static void main(String[] args) throws Exception { int threadCount 50; CountDownLatch latch new CountDownLatch(threadCount); SetString allQueueNos ConcurrentHashMap.newKeySet(); for (int i 0; i threadCount; i) { new Thread(() - { try (Socket socket new Socket(127.0.0.1, 9001); BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream(), UTF-8)); PrintWriter out new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), UTF-8), true)) { out.write(TAKE_NUMBER,A\n); out.flush(); String resp in.readLine(); if (resp ! null resp.startsWith(TAKE_OK)) { String no resp.split(,)[1]; allQueueNos.add(no); } } catch (IOException e) { e.printStackTrace(); } finally { latch.countDown(); } }).start(); } latch.await(); System.out.println(并发取号完成不重复号码数 allQueueNos.size()); }这个脚本的关键点是每个线程都独立创建 Socket 连接真实模拟大厅多台取号机并发请求。ConcurrentHashMap.newKeySet()用来收集所有返回的号码最后检查总数是否等于 50。如果少号就是服务端线程池队列溢出或数据库写入失败如果出现重复号码就是号码生成逻辑需要改。事务之外线程池参数也可以根据压测结果调整。我的参考经验是单机版演示核心线程 4最大线程 8队列 100机房 20 台终端核心线程 16最大线程 32队列 500队列满了之后新的取号请求会触发RejectedExecutionException这时客户端应该提示「系统繁忙」至于排号报表可以从T_QUEUE表按天聚合统计每类业务取号量、平均等待时间、平均办理时间。用 Oracle 的AVG和日期函数能直接查出SELECT TO_CHAR(CREATE_TIME, YYYY-MM-DD) AS BIZ_DATE, BIZ_CODE, COUNT(*) AS TOTAL_NUM, ROUND(AVG(CALL_TIME - CREATE_TIME) * 1440, 2) AS AVG_WAIT_MIN FROM T_QUEUE WHERE CREATE_TIME SYSDATE - 7 GROUP BY TO_CHAR(CREATE_TIME, YYYY-MM-DD), BIZ_CODE ORDER BY BIZ_DATE, TOTAL_NUM DESC;这句话可以放进配套文档里作为演示数据。答辩时能拿出一张按天、按业务类型统计的数据表比单纯说「系统有叫号功能」得分高得多。最后说一个我的个人习惯。每次拿到这种带文档和源码的项目我都会先跑一遍压测再去看界面因为界面能骗人数据库里的号码不能骗人。以后做 Java Socket 项目我也会强制走一遍这套流程确认端口、确认编码、确认线程池、确认号码唯一、最后才连 GUI。这套银行排号系统已经把 C/S 项目的骨架完整搭好了如果你正在准备课程设计或期末项目从它的源码和文档入手会省掉不少查资料的弯路希望帮到你。本文还有配套的精品资源点击获取