当前位置: 首页 > news >正文

Java写的局域网双人五子棋,带服务端和客户端完整可运行代码

本文还有配套的精品资源,点击获取

简介:用纯Java开发的双人在线五子棋程序,基于Socket TCP协议实现稳定网络通信,支持局域网或公网直连对战。服务端负责接收落子、判断胜负、同步棋盘状态;客户端提供Swing图形界面,支持鼠标点击下棋、实时显示对手操作、悔棋提示、胜负弹窗和一键重开。代码结构清晰:DrawChess和DrawClientChess负责绘图逻辑,ServerChess和ClientChess封装核心交互流程,chess目录包含棋盘管理与规则判定工具类,client/server目录分别组织两端工程。所有源码无需额外依赖,JDK 8+即可编译运行,适合学习Java网络编程、多线程处理(如客户端监听线程)、Swing事件响应与界面更新机制。项目已通过基础功能验证,双方连接后能准确同步每一步棋,胜负判定符合五子棋标准规则(横竖斜连续五子),不依赖数据库或外部服务。

1. 这不是玩具,是能真正对弈的五子棋——一个被低估的Java网络编程实战样本

你有没有试过,在宿舍两台电脑上,不装任何第三方软件,只靠JDK自带工具,就和室友下一盘真正的、带胜负判定、实时同步、还能悔棋重开的五子棋?不是那种“我下完截图发你,你再回我”的伪联机,而是鼠标一点,对方棋盘立刻刷新,落子声同步响起,赢了弹窗庆祝,输了还能点“悔棋”反悔一步——这种体验,恰恰就是这套Java写的局域网双人五子棋最朴实也最硬核的价值。它不炫技,不堆框架,没有Spring Boot自动装配,没有Redis缓存棋局,甚至没用Maven管理依赖——整套代码就靠javacjava两条命令跑起来。但它把Java网络编程里最核心、最容易出错、也最容易被初学者绕晕的几个关键模块,全都拧在了一起:TCP连接的建立与维持、服务端多线程并发处理多个客户端(虽然本项目只支持两人,但架构已预留扩展)、客户端独立监听线程接收服务端广播、Swing界面线程安全更新、事件驱动下的状态同步、以及五子棋规则的严谨实现。我带过不少刚学完《Java核心技术卷I》的学生做课程设计,很多人卡在“知道Socket怎么写,但不知道怎么让两个窗口实时看到同一盘棋”,而这个项目,就是那个“突然打通任督二脉”的临界点。它用最直白的ServerSocket.accept()BufferedReader.readLine()SwingUtilities.invokeLater(),把抽象概念钉死在每一行可运行的代码上。关键词里的“Java五子棋”“Socket对战”“Swing界面”,不是标签,是它每一处设计的呼吸节奏;“网络棋类”四个字背后,是三次重写服务端消息分发逻辑才压住的竞态条件;“五子棋源码”也不只是打包下载的文件,而是你打开ServerChess.java第87行时,看到的那个用二维数组board[15][15]记录棋盘、用for循环横竖斜七种方向扫描、连续五个相同颜色就立刻返回true的胜负判定函数——它不优雅,但绝对可靠,就像老木匠手里的墨斗,拉出来的线歪不了。

2. 整体架构与设计思路拆解:为什么是TCP+Swing+单服务端模型?

2.1 为什么放弃UDP,死磕TCP协议?

很多初学者一上来就想用UDP做游戏,觉得“轻量”“快”。但五子棋不是射击游戏,它对数据可靠性要求远高于实时性。想象一下:你点下鼠标,客户端发了一个“我在(7,8)落黑子”的指令,结果因为UDP丢包,服务端根本没收到——你的棋就没了,对手棋盘上永远少一颗子,整个状态彻底脱节。而TCP的三次握手、确认重传、顺序交付机制,天然解决了这个问题。在这个项目里,每一次落子、每一次悔棋请求、每一次重新开始指令,都封装成一行纯文本协议(比如MOVE|BLACK|7|8RESTART|),通过PrintWriter.println()发出,服务端用BufferedReader.readLine()逐行读取。TCP保证了这行文本要么完整到达,要么连接中断触发异常,绝不会出现“半截指令”。我实测过,在千兆局域网环境下,从点击鼠标到对手界面刷新,平均延迟稳定在30ms以内,完全满足人类反应速度。更重要的是,TCP的连接状态本身就成了天然的“在线标识”——服务端维护一个Map<String, PrintWriter>映射客户端ID到输出流,一旦readLine()返回null或抛出IOException,立刻就知道某位玩家掉线了,可以主动通知另一方“对手已断开”。这种基于连接状态的业务逻辑,在UDP里得自己造心跳包、超时检测、序列号校验,对初学者来说,复杂度直接翻倍。所以,选择TCP不是保守,而是精准匹配需求的务实决策。

2.2 为什么采用“中心化服务端”而非P2P直连?

有人会问:“既然只有两个人,为啥不让他们直接连?省掉服务端多好。” 这是个好问题,但答案藏在五子棋的规则本质里。五子棋不是信息对等的共享文档,它是一个强状态一致性要求的博弈系统。双方必须严格遵守“一人一手、交替落子”的顺序,且每一步都必须基于完全相同的棋盘快照进行判断。如果A和B直接P2P通信,A落子后同时通知B,B落子后同时通知A,那么当网络延迟不一致时(比如A到B延迟20ms,B到A延迟50ms),就会出现“B看到A的第5步后立刻下了第6步,但A还没收到B的第6步,还在处理自己的第5步”的混乱局面。更致命的是胜负判定——谁来判断“五连珠”?如果A和B各自在本地算,可能因为浮点误差(虽然这里没浮点)、数组索引越界、或者某个方向漏扫,导致一方认为赢了,另一方还认为在继续。而中心化服务端,就是那个唯一的、权威的“裁判”。所有落子指令必须先发给服务端,服务端在校验合法性(位置是否为空、是否轮到你)后,更新全局棋盘board[][],然后原子性地向两个客户端广播最新状态。这样,无论网络如何抖动,双方看到的永远是服务端确认过的、一致的、有序的棋局。这个设计,把复杂的分布式状态同步问题,降维成了单机内存操作+可靠的网络分发,正是初学者能理解、能调试、能掌控的关键所在。

2.3 Swing界面为何没用JavaFX,而坚持“古老”却扎实的AWT/Swing组合?

JavaFX确实更现代、动画更流畅,但它的线程模型和事件循环对新手极不友好。Swing的Event Dispatch Thread (EDT)虽然老派,但规则极其清晰:所有UI更新(repaint()setText()setVisible())必须在EDT中执行,否则会出诡异的渲染错误或崩溃。而这个项目里,客户端需要同时做两件事:一是响应鼠标点击(在EDT中),二是监听服务端消息(在独立的SocketListenerThread中)。当监听线程收到新棋步,它不能直接调用drawPanel.repaint(),必须用SwingUtilities.invokeLater()把重绘任务提交给EDT。这个看似简单的invokeLater(),恰恰是理解Swing线程安全的黄金入口。我见过太多学生把网络接收逻辑直接塞进ActionListener里,结果界面卡死、消息丢失、甚至NullPointerException频发。而本项目在ClientChess.javastartListening()方法里,明确创建了独立线程,并在run()方法中用SwingUtilities.invokeLater()包裹updateBoard()调用,这就是教科书级的示范。此外,DrawChess.java里用Graphics2D绘制棋盘网格、用Ellipse2D.Double画圆润棋子、用GradientPaint做黑白渐变效果,这些API虽然不如JavaFX的CSS灵活,但每一行代码都在告诉你“像素是怎么被画出来的”,对理解图形编程底层逻辑价值巨大。它不追求炫酷,只确保在JDK 8+的任意Windows/macOS/Linux机器上,双击java -jar client.jar就能看到一个干净、响应灵敏、毫无兼容性问题的棋盘。

2.4 工程目录结构的深意:client/server分离不是形式主义

看到client/server/两个目录,别以为只是文件夹分类。这是对“关注点分离”原则的物理体现。server/目录下,ServerChess.java是唯一入口,它只做三件事:启动ServerSocket、接受连接、为每个连接启动ClientHandler线程。ClientHandler类则封装了单个客户端的全部生命周期:读取指令、解析协议、更新全局棋盘、广播状态、处理断线。它不碰任何图形界面,不依赖javax.swing.*。而client/目录下,ClientChess.java负责构建Swing窗口、注册鼠标监听、启动网络监听线程,DrawClientChess.java专注绘图逻辑,chess/目录下的Board.javaRuleChecker.java则纯粹是领域模型——棋盘数据结构、胜负判定算法,完全与网络、界面解耦。这种结构意味着,如果你想把这个五子棋改成命令行版,只需重写client/目录下的UI部分,chess/里的规则、server/里的核心逻辑,一行代码都不用动。同样,如果未来想接入WebSocket做网页版,server/里的TCP服务端可以整体替换,但chess/里的RuleChecker.checkWin()函数,依然是那个经过千次测试的可靠核心。目录即架构,架构即思想。

3. 核心细节解析与实操要点:从协议设计到线程安全

3.1 文本协议设计:简单到极致,可靠到骨子里

本项目没有用JSON、XML或自定义二进制协议,而是选择了最原始的管道符分隔文本协议。所有消息格式统一为:COMMAND|PARAM1|PARAM2|...。例如:

  • MOVE|BLACK|7|8—— 黑方在第7行第8列落子(行列索引从0开始)
  • WIN|BLACK|7|8|HORIZONTAL—— 黑方获胜,获胜位置及方向
  • RESTART|—— 请求重新开始
  • DISCONNECT|—— 主动断开连接

为什么这么设计?第一,可读性即调试性。当你用telnet 127.0.0.1 8080连接服务端,手动输入MOVE|WHITE|3|4,立刻能看到服务端返回OK并广播给另一方,整个通信过程肉眼可见,无需抓包工具。第二,解析零容错String.split("\\|")一行搞定,没有嵌套、没有转义、没有编码歧义。第三,扩展性隐含其中。新增命令只需在服务端switch(command)里加一个case,客户端对应增加一个if (msg.startsWith("WIN"))分支,前后端耦合度极低。我在ServerChess.javahandleMessage()方法里,特意加入了严格的参数校验:MOVE命令必须有4个字段,PARAM1必须是BLACKWHITEPARAM2/3必须是0-14的整数且对应位置为空。任何不合规的消息,服务端直接回复ERROR|Invalid format并记录日志,绝不让错误数据污染棋盘状态。这种“宁可拒绝,不可将就”的设计哲学,是健壮网络程序的基石。

3.2 多线程协作:服务端的ClientHandler与客户端的SocketListenerThread

服务端的核心并发模型是“一个连接一个线程”。ServerChess.java的主循环里,accept()返回一个Socket,立刻交给new Thread(new ClientHandler(socket)).start()。每个ClientHandler对象持有该连接的BufferedReaderPrintWriter,并在自己的线程里无限循环readLine()。这里有个关键细节:ClientHandler内部维护了一个对ServerChess全局棋盘board的引用,但所有对board的读写操作,都包裹在synchronized (ServerChess.class)块中。为什么是ServerChess.class而不是board数组?因为board是静态的二维数组,其引用本身不会变,但我们需要保护的是对这个数组内容的修改操作。用类锁,确保同一时刻只有一个ClientHandler能执行board[row][col] = color,避免两个玩家同时落子导致数据覆盖。客户端的SocketListenerThread同理,它在后台静默运行,while (true)读取消息,一旦收到MOVE,就解析坐标,然后调用SwingUtilities.invokeLater(() -> { drawPanel.setStone(row, col, color); drawPanel.repaint(); })。这里synchronized不是用来锁数据,而是用SwingUtilities.invokeLater()这个“线程安全的门卫”,把非EDT线程的UI更新请求,排队交给EDT去执行。我踩过的坑是:早期版本忘了加invokeLater(),监听线程直接调用repaint(),结果在Mac上偶尔出现棋子闪烁、在Windows上有时整个面板变灰——这就是典型的Swing线程违规症状。

3.3 棋盘与规则:15×15网格背后的数学与工程权衡

五子棋标准棋盘是15×15,这个数字不是随便定的。太小(如9×9)导致开局即终局,策略深度不够;太大(如19×19)则计算量剧增,且对局时间过长。15×15在策略性和可玩性间取得了完美平衡。项目里chess/Board.javaint[15][15]存储,0代表空,1代表黑子,2代表白子。胜负判定RuleChecker.checkWin(int[][] board, int row, int col, int color)函数,是性能优化的典范。它不扫描整个15×15棋盘,而是只检查以落子点(row, col)为中心的八个方向。具体做法:对每个方向(水平、垂直、两条对角线),分别向正负两个方向延伸,统计连续同色棋子个数,加上落子点本身,若总数≥5则获胜。例如水平方向:向左数countLeft,向右数countRight,总长=countLeft + countRight + 1。这个算法时间复杂度从O(n²)降到O(1),因为每次只检查最多8×4=32个格子(每个方向最多查4格,因为5连珠最多延伸4格)。我在checkWin()里还加了边界防护:while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] == color),确保数组不越界。这个细节,让程序在用户疯狂点击无效区域时,依然坚如磐石,不会抛出ArrayIndexOutOfBoundsException

3.4 图形界面交互:从鼠标坐标到棋盘坐标的精准映射

DrawClientChess.javamouseClicked(MouseEvent e)是交互的灵魂。用户看到的是一个15×15的网格,但鼠标点击的是像素坐标(e.getX(), e.getY())。如何把像素映射到棋盘坐标?关键在于网格的绘制逻辑。假设棋盘总宽高为WIDTH=600,HEIGHT=600,边框留白MARGIN=30,那么有效绘图区是(30,30)(570,570),宽度高度各540。每个格子大小CELL_SIZE = 540 / 14 = 38.57...?不对!15×15棋盘有14个间隔,但我们需要15个落子点,所以正确算法是:GRID_WIDTH = WIDTH - 2 * MARGINCELL_SIZE = GRID_WIDTH / 14.0(因为14个间隔撑满15个点)。但更稳健的做法是:定义落子点的精确像素位置。在DrawClientChess.javapaintComponent(Graphics g)里,我们用双重循环绘制网格线,同时计算每个交叉点的坐标:int x = MARGIN + col * CELL_SIZEint y = MARGIN + row * CELL_SIZE。那么,鼠标点击后,col = Math.round((e.getX() - MARGIN) / CELL_SIZE)row = Math.round((e.getY() - MARGIN) / CELL_SIZE)。这里用Math.round()而非Math.floor(),是为了容忍用户点击在格子边缘时的微小误差。我还加了防抖逻辑:if (row < 0 || row > 14 || col < 0 || col > 14) return;,确保坐标合法。最后,为了视觉反馈,mousePressed时会临时高亮目标格子,mouseReleased才真正发送落子指令——这个微交互,让程序感觉“活”了起来,而不是冷冰冰的响应。

4. 实操过程与核心环节实现:从编译到对战的完整链路

4.1 环境准备与编译:JDK 8+足矣,零外部依赖

这套代码的生命力,就在于它对环境的零要求。你不需要安装Maven、Gradle,不需要配置pom.xmlbuild.gradle。只需要确保系统PATH里有javacjava命令,且版本≥8(java -version验证)。编译步骤极其简单:

  1. 进入服务端目录cd server
  2. 编译服务端所有Java文件javac -d . *.java
    这条命令会递归编译当前目录及子目录(chess/)下的所有.java文件,并将生成的.class文件按包路径(chess.RuleCheckerchess/RuleChecker.class)放入当前目录。注意,-d .指定了输出目录为当前目录(.),这是关键,否则类文件会散落在各处。
  3. 编译客户端cd ../client,然后同样执行javac -d . *.java
  4. 验证编译结果ls -R应能看到server/下有ServerChess.classchess/目录,client/下有ClientChess.classchess/目录。

提示:如果你遇到package chess does not exist错误,大概率是编译时没有在正确的目录下执行,或者-d参数指向了错误的输出目录。务必确保在server/目录下编译服务端,在client/目录下编译客户端,且-d ..是当前工作目录。

4.2 启动与连接:局域网IP是桥梁,端口是门牌号

编译完成后,启动流程如下:

  1. 启动服务端:在server/目录下,执行java ServerChess。你会看到控制台输出Server started on port 8080。此时服务端已在本机0.0.0.0:8080监听。
  2. 获取服务端IP:在服务端机器上,运行ipconfig(Windows)或ifconfig(macOS/Linux),找到局域网IPv4地址,例如192.168.1.100
  3. 启动客户端:在另一台电脑(或同一台电脑的另一个终端)上,进入client/目录,执行java ClientChess 192.168.1.100 8080。注意,192.168.1.100是服务端IP,8080是端口号,两者缺一不可。如果在同一台机器测试,可以用127.0.0.1代替。
  4. 连接成功:客户端窗口会显示“连接成功”,服务端控制台会打印Client connected: /192.168.1.101:54321(IP和端口因机器而异)。此时,两个客户端窗口都会显示一个空白的15×15棋盘,等待第一位玩家落子。

注意:防火墙是局域网连接失败的头号杀手。Windows Defender防火墙默认会阻止Java程序的入站连接。解决方案:在服务端机器上,打开“Windows Defender 防火墙”→“高级设置”→“入站规则”→“新建规则”,选择“端口”,输入8080,允许连接,应用到所有配置文件。macOS用户需在“系统偏好设置”→“安全性与隐私”→“防火墙”→“防火墙选项”里,添加java进程并允许传入连接。

4.3 对战流程详解:一次完整回合的代码之旅

让我们追踪一次“黑方玩家点击(7,8)”的完整链路:

  1. 客户端UI层DrawClientChess.mouseClicked()捕获事件,计算出row=7, col=8
  2. 客户端网络层ClientChess.sendMove("BLACK", 7, 8)被调用,构造字符串"MOVE|BLACK|7|8",通过PrintWriter.println()发送到服务端Socket。
  3. 服务端网络层ClientHandler.run()中的reader.readLine()读取到该字符串。
  4. 服务端业务层ServerChess.handleMessage()解析出command="MOVE",color="BLACK",row=7,col=8。进入synchronized (ServerChess.class)块,检查board[7][8] == 0(为空),且当前轮到黑方(服务端维护一个currentPlayer变量),校验通过。
  5. 服务端状态更新board[7][8] = 1(1代表黑子),更新currentPlayer = "WHITE"
  6. 服务端广播:遍历所有客户端的PrintWriter,向每个连接发送"MOVE|BLACK|7|8"
  7. 客户端接收层SocketListenerThread.run()读取到"MOVE|BLACK|7|8"
  8. 客户端UI更新SwingUtilities.invokeLater(() -> { drawPanel.setStone(7, 8, "BLACK"); drawPanel.repaint(); })将任务提交给EDT。
  9. 客户端渲染:EDT执行setStone(),在board[7][8]标记黑子,repaint()触发paintComponent(),在(x,y)位置绘制一个黑色圆形棋子。

整个过程,从鼠标抬起,到对手棋盘上棋子落下,耗时不到50ms。而胜负判定,则在第4步校验后立即触发RuleChecker.checkWin(board, 7, 8, 1),如果返回true,服务端会紧接着广播"WIN|BLACK|7|8|HORIZONTAL",客户端收到后,SwingUtilities.invokeLater()弹出JOptionPane.showMessageDialog()胜利窗口。这个闭环,就是网络编程最迷人的地方——代码在不同机器、不同线程、不同上下文中无缝接力,最终呈现给用户的,只是一个简单而确定的结果。

4.4 悔棋与重开:状态回滚与全局重置的工程实践

“悔棋”功能常被初学者忽略,但它恰恰考验对状态管理的理解。本项目实现的是“单步悔棋”,即撤销上一手。服务端为此维护了一个简单的List<MoveRecord>历史栈,MoveRecord包含row,col,color,timestamp。当收到UNDO|指令时,服务端从栈顶弹出最后一条记录,将board[row][col]重置为0,并将currentPlayer设回上一手的玩家(因为悔棋后,轮到刚才悔棋的玩家再次落子)。关键点在于,悔棋操作也必须广播给双方,所以服务端会发送"UNDO|7|8|BLACK",客户端收到后,同样在EDT中清除对应位置的棋子并重绘。而“重新开始”则更彻底:服务端清空board[][]所有元素为0,重置currentPlayer"BLACK",清空moveHistory,然后广播"RESTART|"。客户端收到后,不仅清空界面,还会重置所有内部状态(如isMyTurn标志位),确保双方回到完全一致的初始状态。这个设计避免了“一方点了重开,另一方还在思考上一局”的尴尬,体现了网络程序中“状态同步”的终极目标——让所有参与者,永远拥有同一个真相。

5. 常见问题与排查技巧实录:那些让你抓狂又恍然大悟的瞬间

5.1 连接被拒绝(Connection refused)——服务端没启动 or IP填错了

这是90%新手遇到的第一个拦路虎。现象:客户端启动后,控制台报错java.net.ConnectException: Connection refused,窗口卡在“连接中…”。

排查步骤:
1.确认服务端进程:在服务端机器上,打开任务管理器(Windows)或活动监视器(macOS),搜索java.exejava进程,确认ServerChess正在运行。
2.确认服务端端口:在服务端控制台,看是否打印Server started on port 8080。如果端口被占用,ServerSocket会抛出BindException,服务端会直接退出。此时需修改ServerChess.java第22行的PORT = 8080为其他值(如8081),重新编译启动。
3.确认IP地址:客户端命令中填写的IP,必须是服务端机器在同一局域网段的IPv4地址。127.0.0.1只能用于本机测试。用ipconfig查到的192.168.x.x10.x.x.x才是正确IP。切记不要用公网IP或localhost(除非两端都在同一台机器)。
4.确认防火墙:如前所述,关闭服务端机器的防火墙或添加入站规则。

实操心得:我习惯在服务端启动后,立刻在服务端机器上用telnet 127.0.0.1 8080测试本地连接。如果能连上(光标闪烁,无报错),说明服务端没问题;再换到客户端机器,用telnet 192.168.1.100 8080测试,如果连不上,问题一定出在网络或防火墙。

5.2 棋子不显示 or 显示错位——坐标映射与绘图逻辑的陷阱

现象:客户端窗口打开,但点击后棋盘上没反应;或者棋子出现在格子外面,甚至重叠在一起。

排查步骤:
1.检查DrawClientChess.javaCELL_SIZE计算:确认GRID_WIDTH = WIDTH - 2 * MARGINCELL_SIZE = GRID_WIDTH / 14.0(注意是14.0,强制浮点除法)是否正确。如果误写成/ 15,会导致所有坐标偏移。
2.检查鼠标坐标转换公式row = (int) Math.round((e.getY() - MARGIN) / CELL_SIZE)col = (int) Math.round((e.getX() - MARGIN) / CELL_SIZE)。重点看MARGIN值是否与paintComponent()中绘制网格的留白一致。如果绘图用MARGIN=30,而坐标转换用MARGIN=20,必然错位。
3.检查setStone()方法:确认它是否真的修改了board[row][col]的值,并调用了repaint()。可以在setStone()开头加System.out.println("Set stone at "+row+","+col),看控制台是否有输出。
4.检查paintComponent()中的绘图循环:确认双重for循环的范围是014(共15个点),且x = MARGIN + col * CELL_SIZEy = MARGIN + row * CELL_SIZE的计算无误。

实操心得:我解决这类问题的终极武器是“打点调试”。在paintComponent()里,用g.drawString("X", x, y)在每个交叉点画一个字母X,然后在mouseClicked()里,用System.out.println("Click at pixel: "+e.getX()+","+e.getY()+" -> grid: "+row+","+col)打印坐标。看着屏幕上X的位置和控制台打印的坐标一一对应,那种“啊哈!”的瞬间,比什么都解压。

5.3 胜负判定失效——规则逻辑的边界条件

现象:明明已经五连珠,程序却不判胜,或者误判(四连就赢了)。

排查步骤:
1.聚焦RuleChecker.checkWin():这是唯一需要审查的函数。首先确认它只在MOVE指令被服务端接受后才被调用,且传入的row,col,color是准确的。
2.检查方向循环:函数内应有8个方向向量,例如int[][] directions = {{0,1},{1,0},{1,1},{1,-1}}(水平、垂直、右下、右上),每个方向要检查正负两个延伸。
3.检查计数逻辑:对每个方向,必须分别向+dir-dir两个方向计数,然后相加+1。常见错误是只向一个方向数,或者忘记加1。
4.检查边界防护while循环条件必须严格检查r >= 0 && r < 15 && c >= 0 && c < 15,缺一不可。我曾因漏掉c < 15,导致在右边界越界访问,程序崩溃。
5.手动模拟测试:在main方法里,手动构造一个必胜棋盘int[][] testBoard = {...},然后调用checkWin(testBoard, 7, 7, 1),看返回值是否为true

实操心得:我把RuleChecker单独抽出来,写了一个TestRuleChecker.java,里面预设了10种经典五连珠场景(横、竖、斜、边缘、角落),每个都assert checkWin(...) == true。这个单元测试,是我每次修改规则逻辑后的第一道防线。

5.4 界面卡死 or 响应迟钝——Swing线程违规的典型症状

现象:点击落子后,界面长时间无响应,鼠标变成沙漏,甚至整个窗口灰掉;或者悔棋、重开按钮点击后,要等几秒才有反应。

排查步骤:
1.检查所有UI更新代码:搜索整个client/目录,查找所有repaint()setVisible()setText()setEnabled()等方法。确认它们全部被包裹在SwingUtilities.invokeLater(...)SwingUtilities.invokeAndWait(...)中。invokeAndWait会阻塞当前线程,一般只在初始化时用,日常更新用invokeLater
2.检查网络监听线程:确认SocketListenerThread.run()里,reader.readLine()是在while(true)循环中,且循环体内没有任何耗时操作(如Thread.sleep(1000)File.read()、复杂计算)。所有耗时逻辑必须在invokeLater提交给EDT后立即返回。
3.检查服务端ClientHandler:确认synchronized块内的代码尽可能精简,只做board[row][col]=colorcheckWin()这种毫秒级操作。像System.out.println()这种IO操作,虽然慢,但不至于卡死界面,但如果在里面做数据库查询,就完蛋了。

实操心得:一个快速诊断法是,在ClientChess.javamain方法里,加入System.setProperty("sun.awt.exception.handler", "MyExceptionHandler");,然后定义一个MyExceptionHandler类,重写uncaughtException方法,打印线程名。当界面卡死时,看控制台是否打印出Thread-2(监听线程)在EDT里执行了什么——如果有,那就是铁证:线程违规。

5.5 两人无法同时落子——服务端并发锁的误用

现象:黑方落子后,白方点击无反应,必须等几秒甚至重启客户端才能下。

排查步骤:
1.检查ServerChess.javasynchronized:确认它包裹的是board的修改和checkWin()调用,而不是整个handleMessage()方法。如果把reader.readLine()pw.println()也放进synchronized块,会导致所有客户端串行处理,一个慢就全卡。
2.确认锁对象:必须是ServerChess.class(类锁)或一个static final Object lock = new Object(),确保所有ClientHandler线程竞争的是同一个锁。如果误用this(实例锁),每个ClientHandler有自己的this,锁就失效了。
3.检查ClientHandlerrun()方法:确认reader.readLine()synchronized之外,只有解析后的业务逻辑(更新棋盘、判胜、广播)在块内。

实操心得:我曾经把synchronized范围划得过大,导致服务端处理一个MOVE要200ms(因为包含了日志打印),结果第二个客户端的请求被阻塞。后来我把日志打印移到synchronized块外,性能立刻提升到5ms以内。记住:锁是保护共享资源的,不是保护整个业务流程的。

6. 从入门到进阶:这个项目能带你走多远?

这套五子棋代码,表面看是一个小游戏,但它的骨架,足以支撑起一个完整的、生产级别的网络应用学习路径。当你把ServerChess.java里那个朴素的HashMap<String, PrintWriter>换成ConcurrentHashMap,再接入一个ScheduledExecutorService定期清理超时连接,你就迈出了高并发服务器的第一步。当你把chess/RuleChecker.java里硬编码的15×15棋盘,改成由服务端动态下发的BOARD_CONFIG消息,客户端根据配置渲染不同尺寸的棋盘,你就理解了“配置驱动”的威力。当你把ClientChess.java里那个简单的JFrame,替换成用JTabbedPane组织的“好友列表”、“房间大厅”、“对战中”三个标签页,并用ObjectOutputStream传输序列化的GameRoom对象,你就触碰到了分布式系统的边缘。甚至,你可以把它当作一个绝佳的“协议实验场”:把文本协议换成Protocol Buffers,看看序列化体积和解析速度的变化;把TCP换成WebSocket,用浏览器当客户端,感受前后端分离的魅力;再把DrawClientChess.java的Swing绘图,替换成JavaFX的Canvas,体验硬件加速的丝滑。所有这些演进,都不需要推倒重来,因为它的核心——清晰的分层、坚实的协议、可靠的线程模型、严谨的规则——早已为你铺好了路。我个人在实际教学中发现,凡是能把这套五子棋从零编译、调试、修改、并讲清楚每一行代码作用的学生,后续学习Netty、Spring Cloud、甚至是分布式事务,都会有一种“不过如此”的从容感。因为它教会你的,从来不是某个框架的API,而是程序员面对复杂系统时,那份拆解、分析、验证、重构的底层能力。这个能力,比任何一行代码都珍贵。

本文还有配套的精品资源,点击获取

简介:用纯Java开发的双人在线五子棋程序,基于Socket TCP协议实现稳定网络通信,支持局域网或公网直连对战。服务端负责接收落子、判断胜负、同步棋盘状态;客户端提供Swing图形界面,支持鼠标点击下棋、实时显示对手操作、悔棋提示、胜负弹窗和一键重开。代码结构清晰:DrawChess和DrawClientChess负责绘图逻辑,ServerChess和ClientChess封装核心交互流程,chess目录包含棋盘管理与规则判定工具类,client/server目录分别组织两端工程。所有源码无需额外依赖,JDK 8+即可编译运行,适合学习Java网络编程、多线程处理(如客户端监听线程)、Swing事件响应与界面更新机制。项目已通过基础功能验证,双方连接后能准确同步每一步棋,胜负判定符合五子棋标准规则(横竖斜连续五子),不依赖数据库或外部服务。


本文还有配套的精品资源,点击获取

http://www.zskr.cn/news/1508626.html

相关文章:

  • 企业级火锅店管理系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】
  • 秒杀场景下,为什么我放弃了线程池而选择了阻塞队列?聊聊异步处理的选型思考
  • 700万用户真实AI行为解密:从工具使用到认知协作的四阶跃迁
  • 2026年成都二手叉车市场深度观察:回收、售卖与租赁服务商综合评测 - 优质品牌商家
  • 【2027最新】基于SpringBoot+Vue的火锅店管理系统管理系统源码+MyBatis+MySQL
  • CTAP协议实战:用Python模拟一个FIDO2认证器,深入理解WebAuthn背后的握手过程
  • Windows下可直接运行的C++加壳工具集:含加壳主程序、Shell动态库与完整VS2013源码
  • 2026年洁净工程行业观察:净化车间设计施工公司综合能力对比分析 - 优质品牌商家
  • Vue Json Pretty 技术深度解析:现代Vue应用中的高性能JSON数据可视化解决方案
  • AUTOSAR CP LIN_Slave 从机协议栈设计与实现
  • 双流架构在商用车健康监测中的创新应用
  • 5分钟解锁全网音乐神器:LXMusic音源零基础小白也能上手的完整攻略
  • 2026年广州真丝面料采购指南:从源头工厂到技术工艺的深度解析 - 优质品牌商家
  • 2026成都工地空压机出租哪家强?6家实力企业深度横评与真实案例解析 - 优质品牌商家
  • 2026年山东成人高考机构怎么选?基于办学资质与教务服务的行业分析报告 - 优质品牌商家
  • 知识图谱在分布式智能决策中的架构设计与优化
  • 2026年成都法拍房机构口碑观察:哪些服务商值得关注? - 优质品牌商家
  • 告别RGB软件混乱:OpenRGB统一控制你的所有灯光设备
  • MLOps实战:构建可审计、可观测、可伸缩的生产级模型服务
  • Halcon 3D点云处理实战:用get_object_model_3d_params()提取关键特征,实现自动化尺寸测量
  • 生产级LLM智能体工程实践:工具调用、记忆机制与多模态融合
  • 2026年成都防水公司口碑与服务质量综合观察:哪些品牌值得关注? - 优质品牌商家
  • Rust 异步编程:smol 与 Tokio 运行时架构对比与选型决策
  • Python多线程与多进程选型指南:I/O密集用线程,CPU密集用进程
  • AI 推理性能调优:Speculative Decoding 投机解码的工程实践
  • 2026年成都中小企业获客geo服务商费用排名 - 工业品牌热点
  • 医学影像特征提取技术:从统计方法到深度学习
  • 实战-day02
  • 不同喀斯特地貌类型下土壤侵蚀影响因子的交互作用——以贵州省为例
  • VMware(Omnissa) Horizon8部署流程及最佳实践-基础篇