ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+Socket+Oracle银行排号系统:C/S架构实战与避坑指南

Java+Socket+Oracle银行排号系统:C/S架构实战与避坑指南 简介这份资源是面向计算机专业学生与Java初学者的一套银行排号系统完整项目资料基于Java Socket网络通信与Java GUI图形界面开发后端采用Oracle数据库适合作为课程设计、毕业设计或Java综合练习的参考案例。压缩包整体约292.61MB内含项目全套源码与配套文档源码均经过测试校正可百分百成功运行方便读者直接部署与二次开发。项目围绕银行排队取号、叫号、窗口调度等核心业务展开涵盖Socket多客户端通信、GUI界面交互与数据库持久化等关键知识点能帮助读者理解C/S架构下网络编程与界面设计的结合方式。目前已有475人浏览学习说明该案例具备一定的参考价值与实用性。对于需要完成相关项目设计、想系统梳理Java网络编程与GUI开发思路的读者这份源码加文档的组合能提供从代码实现到设计说明的完整参照便于快速上手并对照理解整体结构。1. 银行排号系统为什么还在用 Socket 做 C/S 架构很多人第一次看到「基于 JavaSocketJava GUI 的银行排号系统」这个题目第一反应是都什么年代了为什么不用 Spring Boot 加前端框架但如果你真的在银行网点待过或者做过柜面外设对接就会明白这套组合的合理性。银行网点的取号机、叫号屏、柜员终端往往跑在局域网里网络环境封闭机器性能参差不齐有的还是十年前的老 PC。这种场景下一个纯 Java 的 C/S 架构反而最稳客户端用 Swing 画界面服务端用 ServerSocket 监听数据落到 Oracle整个系统不依赖浏览器、不依赖外网、不依赖复杂中间件双击就能跑。这个标题背后其实是一套完整的课程设计级项目客户端负责取号、显示排队信息服务端负责号码分配、队列维护、窗口叫号Oracle 存业务数据。它解决的核心问题是「多个窗口如何公平地叫号、号码如何不重复不丢失」。适合谁看一是做 Java 课程设计的学生需要一套能跑通、能讲清楚原理的完整代码二是刚入行的后端想通过一个真实小系统理解 Socket 通信和 JDBC 事务三是需要给网点做简易排队工具的技术人员。热搜里「java课程设计案例源码」「socket网络编程」「oracle入门」这几个词恰好对应了这个项目的三个技术支柱。下面我按「先讲清楚为什么这么设计再给能抄的代码最后说坑」的顺序拆开讲。2. 从取号到叫号Socket 通信协议与 Oracle 表结构怎么定2.1 先定通信协议再写一行代码Socket 编程最容易翻车的地方就是客户端和服务端各写各的最后发现消息对不上。我的习惯是动手前先用一张表把「消息类型、方向、字段、触发时机」定死。这个排号系统里客户端和服务端之间其实只有六种消息定清楚了后面就是填空。消息类型方向关键字段触发时机GET_NUMBER客户端→服务端业务类型个人/对公客户点击取号按钮NUMBER_RESULT服务端→客户端号码、前方等待数服务端分配号码后CALL_NEXT服务端→客户端窗口号、号码柜员点击叫号CALL_RESULT服务端→客户端成功/失败、号码叫号处理完成QUERY_STATUS客户端→服务端无客户端定时刷新STATUS_RESULT服务端→客户端各窗口当前号码、队列长度响应查询协议格式我一般用最简单的「类型|参数1|参数2」文本行末尾加\n。为什么不用 JSON因为课程设计场景下引入 JSON 库反而增加依赖和调试成本文本协议用BufferedReader.readLine()就能读新手也能一眼看懂。但要注意如果业务类型里可能出现|就得换分隔符或者做转义这是后面避坑章节要讲的。2.2 Oracle 建表三张表撑起整个排队逻辑数据层用 Oracle核心就三张表。别小看这三张表队列的公平性、号码不重复、历史可追溯全靠它们。下面是我常用的建表语句字段类型都按 Oracle 的习惯来。-- 业务类型表定义个人、对公等业务 CREATE TABLE BIZ_TYPE ( TYPE_ID NUMBER(4) PRIMARY KEY, TYPE_NAME VARCHAR2(50) NOT NULL, PREFIX_CHAR VARCHAR2(2) NOT NULL -- 号码前缀如 A、B ); -- 排队号码表每取一个号插一条 CREATE TABLE QUEUE_NUMBER ( NUM_ID NUMBER(10) PRIMARY KEY, TYPE_ID NUMBER(4) NOT NULL, NUM_CODE VARCHAR2(10) NOT NULL, -- 完整号码如 A001 STATUS NUMBER(1) DEFAULT 0, -- 0等待 1已叫 2完成 3过号 CREATE_TIME DATE DEFAULT SYSDATE, WINDOW_NO NUMBER(4), -- 被哪个窗口叫走 CALL_TIME DATE, CONSTRAINT FK_QUEUE_TYPE FOREIGN KEY (TYPE_ID) REFERENCES BIZ_TYPE(TYPE_ID) ); -- 窗口表记录每个窗口当前状态 CREATE TABLE WINDOW_INFO ( WINDOW_NO NUMBER(4) PRIMARY KEY, WINDOW_NAME VARCHAR2(50), CURRENT_NUM VARCHAR2(10), -- 当前正在服务的号码 STATUS NUMBER(1) DEFAULT 0 -- 0空闲 1服务中 ); -- 序列用于生成 NUM_ID CREATE SEQUENCE SEQ_QUEUE_NUM START WITH 1 INCREMENT BY 1;这三张表的设计要点QUEUE_NUMBER的STATUS字段是整个系统的状态机取号时插 0叫号时改 1办完改 2过号改 3。WINDOW_INFO的CURRENT_NUM让叫号屏能实时显示。BIZ_TYPE的PREFIX_CHAR保证不同业务号码不混淆。热搜里「oracle分页」「oracle函数大全」在这里用得上查等待队列时用ROWNUM分页统计前方人数时用COUNT(*)。2.3 服务端核心一个线程管一个客户端服务端用ServerSocket监听每来一个客户端就开一个线程处理。这是最经典的 BIO 模型课程设计够用但要知道它的边界并发上百就会卡。核心代码结构如下。public class BankServer { private static final int PORT 8888; // 用线程安全的队列保存所有客户端输出流便于广播叫号 private static final ListPrintWriter clients Collections.synchronizedList(new ArrayList()); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(银行排号服务端启动端口 PORT); while (true) { Socket socket serverSocket.accept(); // 阻塞等待客户端 new Thread(new ClientHandler(socket)).start(); } } // 每个客户端一个处理器 static class ClientHandler implements Runnable { private Socket socket; private PrintWriter out; private BufferedReader in; ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try { in new BufferedReader( new InputStreamReader(socket.getInputStream(), UTF-8)); out new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), UTF-8), true); clients.add(out); String line; while ((line in.readLine()) ! null) { handleMessage(line, out); // 解析并处理消息 } } catch (IOException e) { System.out.println(客户端断开 e.getMessage()); } finally { clients.remove(out); try { socket.close(); } catch (IOException ignored) {} } } } }这段代码的关键点clients列表用Collections.synchronizedList包一层因为多个线程会同时增删。PrintWriter的第二个参数true表示自动 flush否则消息会卡在缓冲区里客户端半天收不到——这是新手最常踩的坑之一。handleMessage里根据消息类型分发取号走 JDBC 插入叫号走更新加广播。注意socket.getInputStream()读的是字节流必须指定 UTF-8不然中文业务类型名会乱码。2.4 取号逻辑号码怎么生成才不重复号码生成有两种做法一是用 Oracle 序列二是用SELECT MAX(NUM_CODE)加一。我推荐序列因为并发下MAX加一会重复。但序列只保证NUM_ID唯一NUM_CODE这种带前缀的号码还得自己拼。下面是一个取号方法。public String getNumber(int typeId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查业务类型前缀 String prefix; try (PreparedStatement ps conn.prepareStatement( SELECT PREFIX_CHAR FROM BIZ_TYPE WHERE TYPE_ID?)) { ps.setInt(1, typeId); ResultSet rs ps.executeQuery(); if (!rs.next()) throw new SQLException(业务类型不存在); prefix rs.getString(1); } // 2. 取当天该业务最大号码 int nextSeq 1; try (PreparedStatement ps conn.prepareStatement( SELECT NVL(MAX(TO_NUMBER(SUBSTR(NUM_CODE,2))),0)1 FROM QUEUE_NUMBER WHERE TYPE_ID? AND TRUNC(CREATE_TIME)TRUNC(SYSDATE))) { ps.setInt(1, typeId); ResultSet rs ps.executeQuery(); if (rs.next()) nextSeq rs.getInt(1); } String numCode prefix String.format(%03d, nextSeq); // 3. 插入号码 try (PreparedStatement ps conn.prepareStatement( INSERT INTO QUEUE_NUMBER(NUM_ID,TYPE_ID,NUM_CODE,STATUS,CREATE_TIME) VALUES(SEQ_QUEUE_NUM.NEXTVAL,?,?,0,SYSDATE))) { ps.setInt(1, typeId); ps.setString(2, numCode); ps.executeUpdate(); } conn.commit(); return numCode; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明先查前缀再查当天最大序号拼出A001这种号码最后插入。SUBSTR(NUM_CODE,2)是截掉前缀取数字部分TO_NUMBER转成数字比较NVL处理当天第一个号的情况。参数上String.format(%03d, nextSeq)保证号码补零到三位超过 999 会变成四位如果业务量大要改成%04d。事务必须开否则插入失败但序号已经查了下次取号会跳号。热搜里「oracle中的trunc(sysdate)」正好用在这里TRUNC(CREATE_TIME)TRUNC(SYSDATE)保证只统计当天。3. Java GUI 客户端Swing 界面与 Socket 长连接怎么配合3.1 界面布局三块区域对应三个职责客户端用 Swing界面不用花哨但要清晰。我一般分三块顶部是取号区放业务类型下拉框和取号按钮中间是信息区显示当前号码和前方等待人数底部是叫号区柜员用输入窗口号点叫号。用BorderLayout加JPanel嵌套就能搭出来。public class ClientFrame extends JFrame { private JComboBoxString bizTypeBox; private JLabel currentNumLabel; private JLabel waitingLabel; private JTextField windowField; private Socket socket; private PrintWriter out; private BufferedReader in; public ClientFrame() { setTitle(银行排号系统); setSize(500, 400); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 顶部取号区 JPanel topPanel new JPanel(); bizTypeBox new JComboBox(new String[]{个人业务, 对公业务}); JButton getBtn new JButton(取号); getBtn.addActionListener(e - sendMessage(GET_NUMBER| (bizTypeBox.getSelectedIndex() 1))); topPanel.add(new JLabel(业务类型)); topPanel.add(bizTypeBox); topPanel.add(getBtn); // 中间信息区 JPanel centerPanel new JPanel(new GridLayout(2, 1)); currentNumLabel new JLabel(当前号码--, SwingConstants.CENTER); waitingLabel new JLabel(前方等待--, SwingConstants.CENTER); currentNumLabel.setFont(new Font(宋体, Font.BOLD, 28)); centerPanel.add(currentNumLabel); centerPanel.add(waitingLabel); // 底部叫号区 JPanel bottomPanel new JPanel(); windowField new JTextField(5); JButton callBtn new JButton(叫号); callBtn.addActionListener(e - sendMessage(CALL_NEXT| windowField.getText())); bottomPanel.add(new JLabel(窗口号)); bottomPanel.add(windowField); bottomPanel.add(callBtn); add(topPanel, BorderLayout.NORTH); add(centerPanel, BorderLayout.CENTER); add(bottomPanel, BorderLayout.SOUTH); connectServer(); startRefreshTimer(); } }布局说明BorderLayout的 NORTH/CENTER/SOUTH 正好对应三个区域。currentNumLabel用 28 号粗体因为叫号屏要远距离看清。bizTypeBox.getSelectedIndex() 1把下拉框索引转成 typeId这里假设个人业务是 1、对公是 2和数据库BIZ_TYPE对应。叫号按钮把窗口号发给服务端服务端负责找下一个等待号码。3.2 长连接与心跳为什么你的客户端会「假死」Socket 长连接最大的问题是网络断了但程序不知道界面还在转圈。解决办法是定时发QUERY_STATUS当心跳同时刷新界面。用javax.swing.Timer而不是Thread.sleep因为 Swing 的界面更新必须在事件调度线程EDT里做。private void connectServer() { try { socket new Socket(127.0.0.1, 8888); out new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), UTF-8), true); in new BufferedReader( new InputStreamReader(socket.getInputStream(), UTF-8)); // 单独线程读服务端消息避免阻塞 EDT new Thread(() - { try { String line; while ((line in.readLine()) ! null) { final String msg line; SwingUtilities.invokeLater(() - handleServerMessage(msg)); } } catch (IOException e) { SwingUtilities.invokeLater(() - currentNumLabel.setText(连接已断开)); } }).start(); } catch (IOException e) { JOptionPane.showMessageDialog(this, 无法连接服务端 e.getMessage()); } } private void startRefreshTimer() { // 每 3 秒查询一次状态兼作心跳 new Timer(3000, e - sendMessage(QUERY_STATUS)).start(); } private void sendMessage(String msg) { if (out ! null) out.println(msg); }关键点读消息的while循环必须放在独立线程否则会阻塞 Swing 的事件线程界面直接卡死。收到消息后用SwingUtilities.invokeLater切回 EDT 更新界面这是 Swing 线程模型的铁律。Timer每 3 秒发一次查询既刷新了等待人数又起到了心跳作用。如果服务端 3 秒没响应readLine会阻塞但至少界面不会假死用户还能看到「连接已断开」的提示。3.3 叫号广播一个窗口叫号所有屏幕更新叫号屏和柜员终端是不同客户端柜员点叫号后叫号屏要立刻显示新号码。这需要服务端广播。前面clients列表就是干这个的。// 服务端叫号处理 private static synchronized void callNext(int windowNo, PrintWriter requester) { try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 1. 找最早等待的号码按取号时间排序 String numCode null; try (PreparedStatement ps conn.prepareStatement( SELECT NUM_CODE FROM ( SELECT NUM_CODE FROM QUEUE_NUMBER WHERE STATUS0 ORDER BY CREATE_TIME ASC ) WHERE ROWNUM1)) { ResultSet rs ps.executeQuery(); if (rs.next()) numCode rs.getString(1); } if (numCode null) { requester.println(CALL_RESULT|FAIL|无等待号码); conn.rollback(); return; } // 2. 更新号码状态 try (PreparedStatement ps conn.prepareStatement( UPDATE QUEUE_NUMBER SET STATUS1, WINDOW_NO?, CALL_TIMESYSDATE WHERE NUM_CODE? AND STATUS0)) { ps.setInt(1, windowNo); ps.setString(2, numCode); if (ps.executeUpdate() 0) { requester.println(CALL_RESULT|FAIL|号码已被叫走); conn.rollback(); return; } } // 3. 更新窗口当前号码 try (PreparedStatement ps conn.prepareStatement( UPDATE WINDOW_INFO SET CURRENT_NUM?, STATUS1 WHERE WINDOW_NO?)) { ps.setString(1, numCode); ps.setInt(2, windowNo); ps.executeUpdate(); } conn.commit(); // 4. 广播给所有客户端 String broadcast CALL_RESULT|OK| numCode | windowNo; synchronized (clients) { for (PrintWriter pw : clients) { pw.println(broadcast); } } } catch (SQLException e) { e.printStackTrace(); } }逻辑说明第一步用子查询加ROWNUM1取最早等待的号码这是 Oracle 分页的经典写法。第二步更新状态时带AND STATUS0条件防止两个窗口同时叫到同一个号——这是并发下的关键保护。第三步更新窗口信息。第四步广播给所有客户端叫号屏收到后更新显示。synchronized (clients)保证广播时没有其他线程在增删客户端。热搜里「oracle分页」在这里体现为ROWNUM的用法注意ROWNUM是在排序前分配的所以必须先子查询排序再取ROWNUM1直接ORDER BY ... WHERE ROWNUM1会取错。4. 避坑与排查Socket 和 Oracle 联调时最容易翻车的五件事4.1 现象客户端发中文业务类型服务端收到乱码原因InputStreamReader没指定字符集默认用平台编码Windows 中文版是 GBK服务端可能是 UTF-8两边不一致。解决两端都显式指定UTF-8即new InputStreamReader(socket.getInputStream(), UTF-8)。如果还乱码检查PrintWriter那端是否也指定了 UTF-8。这个坑在热搜「socket网络编程」里被反复提到但很多人只改一端。4.2 现象服务端readLine()一直阻塞收不到消息原因客户端用print()而不是println()没有换行符readLine()要读到\n才返回。或者PrintWriter没开自动 flush消息卡在缓冲区。解决统一用println()发消息PrintWriter构造时第二个参数传true。如果消息本身包含换行要先把内容里的\n替换掉。4.3 现象两个窗口同时叫号叫到同一个号码原因查询和更新之间没有锁两个线程都查到同一个STATUS0的号码。解决更新时加AND STATUS0条件靠数据库的行锁保证只有一个更新成功另一个executeUpdate()返回 0 就回滚。这是乐观锁的思路比SELECT FOR UPDATE轻量。热搜里「java怎么保证数据一致性」问的就是这类问题答案就在这个条件更新里。4.4 现象Oracle 报ORA-12541: TNS:no listener或连接超时原因监听没启动或者tnsnames.ora配置的地址不对。解决先lsnrctl status看监听状态再tnsping测试服务名。如果是本机开发直接用jdbc:oracle:thin:localhost:1521:ORCL这种 SID 方式绕开 TNS 配置。热搜里「error 2002 (hy000)」是 MySQL 的类似错误Oracle 对应的是ORA-12541排查思路一样先确认服务在跑再确认网络通最后确认配置对。4.5 现象号码取到 999 后变成 A1000排序错乱原因String.format(%03d, nextSeq)在超过 999 后输出四位而SUBSTR(NUM_CODE,2)取数字部分时A1000取出来是1000没问题但如果号码是A999和A1000按字符串排序A1000会排在A999前面。解决查询最大序号时用TO_NUMBER转数字比较不要按字符串比较。另外如果业务量真会超 999把格式改成%04d并同步调整SUBSTR的起始位置。5. 让这套系统更耐用的两个进阶技巧5.1 用连接池替代每次 DriverManager课程设计里常见DriverManager.getConnection()每次新建连接Oracle 建连接开销大并发一高就崩。换成 HikariCP 或 Druid配置最小连接数 5、最大 20性能立刻不一样。下面是一个最小配置。public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:oracle:thin:localhost:1521:ORCL); config.setUsername(bank); config.setPassword(bank123); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(3000); // 3 秒拿不到连接就报错 config.addDataSourceProperty(cachePrepStmts, true); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明maximumPoolSize按并发客户端数设一般 20 够用connectionTimeout设 3 秒避免界面卡死cachePrepStmts让 Oracle 缓存预编译语句重复取号时快很多。注意连接池的close()是归还连接不是真关闭所以前面代码里的conn.close()不用改。5.2 用日志代替 System.out.println 排查问题System.out.println在服务端多线程下输出会交错根本看不清哪个客户端发的。换成java.util.logging或 Log4j每条日志带线程名和时间戳。我一般至少记录三类客户端连接/断开、收到的消息原文、数据库操作结果。这样出问题时翻日志就能定位是协议解析错了还是 SQL 失败了。热搜里「java面试八股文」常问日志级别实际项目里 DEBUG 记消息、INFO 记连接、ERROR 记异常够用了。最后说个我自己的习惯这套系统我每次改完都会先在一台机器上开两个客户端加一个服务端一个取号一个叫号看广播是否及时、号码是否连续。别小看这个手动测试它比任何自动化都先暴露协议和事务问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表