
简介这是一套面向计算机专业毕业生的Java五子棋手机网络对战游戏毕设参考包适合正在准备毕业设计、希望系统练习Java开发与网络编程的学生也可供想了解游戏设计流程的开发者借鉴。资源以软件源码与论文文档为核心涵盖Java基础、Swing/JavaFX界面编程、多线程、Socket网络通信、棋盘数据结构与AI搜索算法、胜负判断与悔棋逻辑以及需求分析、系统架构、模块设计和测试计划等文档撰写要点。压缩包为zip格式整体约5.55MB文件类型以源码与说明文档为主源码对应客户端与服务器实现文档则支撑论文写作与答辩准备。目前已有95人学习下载。通过这份资料读者可获得一套可运行的网络对战游戏实现思路、完整的毕设文档框架以及网络同步、线程调度与算法落地的排错参考便于对照复现并迁移到自己的课题中。1. 从一份 JAVA 五子棋网络对战源码说起它到底能帮你解决什么如果你正在准备计算机毕设选题又卡在“想做游戏方向但不知道从哪下手”那 JAVA 五子棋手机网络对战游戏这个方向值得认真看一眼。它不像大型手游那样需要美术、引擎、服务器集群一整套班底核心逻辑就是棋盘状态、落子判定、胜负检测加一套网络通信一个人一台电脑就能跑通全流程。更关键的是它天然覆盖了毕设答辩最容易被追问的几个点Socket 通信、多线程并发、客户端与服务端状态同步、UI 与逻辑分离。换句话说你交上去的不只是一个能下棋的程序而是一份能讲清楚“为什么这么设计”的工程材料。这份源码加文档的组合适合三类人一是时间紧、需要一套能跑起来再改的毕设底座的同学二是想借五子棋练手 Java 网络编程的初学者三是已经写完代码但不知道怎么把设计思路写成文档的人。下面我按“先跑通、再拆解、后避坑”的顺序把这类项目的落地路径讲清楚。2. 五子棋网络对战的核心机制棋盘、通信、并发三件事2.1 棋盘状态怎么存二维数组还是位运算五子棋的棋盘本质是一个 15×15 的网格每个格子有三种状态空、黑子、白子。最直观的存法是二维整型数组0表示空1表示黑2表示白。这种写法可读性最好新手一眼就能看懂调试时打印出来也直观。// 15x15 棋盘0空 1黑 2白 public static final int SIZE 15; private int[][] board new int[SIZE][SIZE]; // 落子先判断该位置是否为空 public boolean placeStone(int row, int col, int color) { if (row 0 || row SIZE || col 0 || col SIZE) { return false; // 越界 } if (board[row][col] ! 0) { return false; // 已有子 } board[row][col] color; return true; }这段代码里SIZE是棋盘边长改成 19 就变成 19 路棋盘但五子棋标准就是 15。placeStone返回布尔值是为了让调用方知道落子是否成功网络对战里这个返回值要回传给对手否则两边棋盘会不一致。有人会问能不能用一维数组或位运算压缩。可以一维数组board[row * SIZE col]省一点内存位运算用两个long存黑白子能压到极致。但毕设场景下15×15 才 225 个格子内存根本不是瓶颈可读性远比省那几个字节重要。我一般建议先用二维数组把逻辑跑通真要做性能优化再换。2.2 胜负判定从落子点向四个方向扫描胜负判定是五子棋最核心的算法。每落一子只需要检查这个子所在的横、竖、左斜、右斜四条线看是否有连续五个同色。不需要全盘扫描那样效率低还容易写错。// 检查以 (row,col) 为起点沿方向 (dr,dc) 是否有五连 private boolean checkDirection(int row, int col, int dr, int dc, int color) { int count 1; // 当前子本身算一个 // 正方向延伸 for (int i 1; i 5; i) { int r row dr * i, c col dc * i; if (r 0 || r SIZE || c 0 || c SIZE || board[r][c] ! color) break; count; } // 反方向延伸 for (int i 1; i 5; i) { int r row - dr * i, c col - dc * i; if (r 0 || r SIZE || c 0 || c SIZE || board[r][c] ! color) break; count; } return count 5; } // 四个方向横、竖、主对角、副对角 public boolean checkWin(int row, int col, int color) { return checkDirection(row, col, 0, 1, color) // 横 || checkDirection(row, col, 1, 0, color) // 竖 || checkDirection(row, col, 1, 1, color) // 主对角 || checkDirection(row, col, 1, -1, color); // 副对角 }dr和dc是方向增量横是(0,1)竖是(1,0)主对角(1,1)副对角(1,-1)。正反两个方向各扫最多 4 格加上当前子最多 9 格常数级复杂度。这里有个容易翻车的点反方向扫描时坐标是row - dr * i如果写成row dr * i就变成只往一个方向数五连判定会漏。2.3 网络通信选型Socket 还是 WebSocket手机网络对战通信层有两个常见选择。一是原生 SocketTCP 长连接Java 的ServerSocket和Socket直接搞定适合毕设展示底层原理。二是 WebSocket协议更轻但需要引入额外库对纯 Java 项目来说反而增加复杂度。毕设场景我推荐原生 Socket。原因很直接答辩老师问“你怎么实现网络通信的”你能从accept()讲到InputStream再讲到自定义协议这条链路清晰且完全在 Java 标准库里。WebSocket 虽然现代但底层被框架封装了讲起来反而虚。// 服务端监听端口每个客户端一个线程 ServerSocket serverSocket new ServerSocket(8888); while (true) { Socket client serverSocket.accept(); // 阻塞等待连接 new Thread(new ClientHandler(client)).start(); // 每连接一线程 }8888是监听端口客户端连接时要填服务端的局域网 IP 加这个端口。accept()会阻塞所以必须放在循环里。每个客户端分配一个线程这是最朴素的多线程模型够用但连接数一多就吃力后面避坑章节会讲怎么改。2.4 自定义通信协议消息怎么定义才不乱网络对战最容易乱的地方是消息格式。两边各发各的字段对不上调试时就是黑匣子。我的做法是定一个极简文本协议每条消息一行用竖线分隔字段。MOVE|3|7|1 表示在(3,7)落子颜色1(黑) WIN|1 表示颜色1获胜 CHAT|hello 聊天消息 READY|1 玩家1准备服务端收到MOVE后先校验合法性再广播给对手。这种文本协议的好处是抓包直接能看懂用telnet就能手动测试。缺点是解析要自己做但五子棋消息类型就几种写个split(\\|)足够。// 解析消息 String[] parts line.split(\\|); switch (parts[0]) { case MOVE: int row Integer.parseInt(parts[1]); int col Integer.parseInt(parts[2]); int color Integer.parseInt(parts[3]); // 校验并转发 break; case WIN: // 处理胜利 break; }split的参数是正则竖线在正则里是特殊字符必须转义成\\|这是新手最常踩的坑之一不转义会按每个字符拆MOVE都能被拆成四个单字母。3. 从零跑通一套五子棋对战环境、服务端、客户端三步走3.1 环境准备与项目结构这类项目通常是纯 Java SE不需要 Spring 那套重家伙。JDK 8 或以上都行IDE 用 IntelliJ IDEA 或 Eclipse 都可以。项目结构建议按功能分包别全堆在一个目录里。gomoku/ ├── src/ │ ├── common/ # 协议常量、消息封装 │ │ └── Protocol.java │ ├── server/ # 服务端 │ │ ├── GameServer.java │ │ └── ClientHandler.java │ ├── client/ # 客户端 │ │ ├── GameClient.java │ │ └── GamePanel.java │ └── model/ # 棋盘逻辑 │ └── Board.java └── doc/ # 设计文档common包放两边都要用的东西比如协议里的消息类型常量。model包放纯逻辑不依赖网络和 UI这样单元测试好写。server和client各自独立。这个分法的好处是答辩时你能清楚地说“逻辑层和通信层解耦了”。3.2 服务端连接管理、消息转发、胜负仲裁服务端要做三件事接受连接、维护玩家配对、转发落子消息。最简单的模型是两个玩家一对服务端只做中转和合法性校验。public class ClientHandler implements Runnable { private Socket socket; private BufferedReader in; private PrintWriter out; private static ListClientHandler players new ArrayList(); public ClientHandler(Socket socket) throws IOException { this.socket socket; this.in new BufferedReader(new InputStreamReader(socket.getInputStream())); this.out new PrintWriter(socket.getOutputStream(), true); } Override public void run() { try { synchronized (players) { players.add(this); // 通知当前玩家分配的颜色 out.println(COLOR| (players.size() % 2 1 ? 1 : 2)); } String line; while ((line in.readLine()) ! null) { broadcast(line); // 转发给所有玩家 } } catch (IOException e) { // 连接断开处理 } finally { synchronized (players) { players.remove(this); } } } private void broadcast(String msg) { synchronized (players) { for (ClientHandler p : players) { p.out.println(msg); } } } }players是静态列表所有连接共享。synchronized块保证多线程下增删不会出错这是必须的否则两个玩家同时连接时列表可能损坏。out.println自动加换行对应客户端的readLine。COLOR消息告诉客户端自己执黑还是执白奇数个玩家是黑偶数是白。服务端还要做胜负仲裁。客户端自己判定胜负不可信万一有人改客户端就能作弊。正确做法是服务端在转发MOVE前先在自己的棋盘上落子并判定赢了就广播WIN。3.3 客户端Swing 界面与网络线程分离客户端用 Swing 画棋盘最省事一个JPanel重写paintComponent就能画格子和棋子。关键点是网络接收必须放在独立线程不能阻塞 UI 线程否则界面会卡死。public class GamePanel extends JPanel { private Board board new Board(); private GameClient client; Override protected void paintComponent(Graphics g) { super.paintComponent(g); int cell 30; // 每格像素 // 画网格线 for (int i 0; i 15; i) { g.drawLine(20, 20 i * cell, 20 14 * cell, 20 i * cell); g.drawLine(20 i * cell, 20, 20 i * cell, 20 14 * cell); } // 画棋子 for (int r 0; r 15; r) { for (int c 0; c 15; c) { if (board.get(r, c) 1) { g.setColor(Color.BLACK); g.fillOval(20 c * cell - 12, 20 r * cell - 12, 24, 24); } else if (board.get(r, c) 2) { g.setColor(Color.WHITE); g.fillOval(20 c * cell - 12, 20 r * cell - 12, 24, 24); } } } } }cell是格子像素大小20是边距。棋子画在交叉点上所以要减去半径12。paintComponent里不要做网络操作只负责画。网络消息到达后更新board然后调用repaint()触发重绘。// 网络接收线程 new Thread(() - { try { String line; while ((line in.readLine()) ! null) { String[] parts line.split(\\|); if (MOVE.equals(parts[0])) { int r Integer.parseInt(parts[1]); int c Integer.parseInt(parts[2]); int color Integer.parseInt(parts[3]); board.placeStone(r, c, color); SwingUtilities.invokeLater(() - repaint()); // 切回 UI 线程 } } } catch (IOException e) { /* 处理断线 */ } }).start();SwingUtilities.invokeLater是关键Swing 不是线程安全的所有 UI 更新必须在事件分发线程做。直接在网络线程里调repaint()有时看着没事但高并发下会出玄学问题界面偶尔花屏或卡住。3.4 联调本机双开与局域网真机测试跑通的第一步是本机双开。服务端先启动然后开两个客户端连127.0.0.1:8888两边各下一子看对方棋盘是否同步。这一步能过说明协议和转发逻辑没问题。第二步是局域网真机测试。手机和电脑连同一个 WiFi服务端跑在电脑上手机客户端填电脑的局域网 IP。这里最常见的翻车是防火墙拦截Windows 默认会拦入站连接需要在防火墙里放行 8888 端口或者临时关掉防火墙测试。# Windows 查看本机局域网 IP ipconfig # 放行端口管理员权限 netsh advfirewall firewall add rule namegomoku dirin actionallow protocolTCP localport8888ipconfig找到 IPv4 地址通常是192.168.x.x。手机客户端填这个地址。如果连不上先ping一下电脑 IP通了再查防火墙这是排查顺序。4. 网络对战最容易翻车的地方五个血泪踩坑记录4.1 现象两边棋盘不一致一方看到有子另一方没有原因通常是消息丢失或顺序错乱。TCP 本身保证顺序但如果服务端用了多个线程分别给两个客户端发消息就可能出现一个先收到一个后收到中间还夹着别的消息。更隐蔽的是客户端在 UI 线程里直接读网络读一半去重绘剩下的消息被吞了。解决所有消息走同一个PrintWriter服务端广播时加锁保证顺序。客户端网络读取单独一个线程读完完整一行再交给 UI。协议里每条消息以换行结尾readLine天然按行读不会读一半。4.2 现象程序跑一会儿就卡死点哪都没反应原因是 UI 线程被阻塞。常见写法是在按钮点击事件里直接socket.getInputStream().read()这是同步阻塞调用没数据就死等整个界面冻结。解决网络 IO 全部放独立线程UI 线程只做界面更新。用SwingUtilities.invokeLater把更新请求丢回 UI 线程。记住一条UI 线程里不出现任何read、accept、sleep。4.3 现象第二个玩家连上后第一个玩家掉线原因是players列表没加同步两个线程同时add导致列表内部结构损坏或者遍历时被修改抛ConcurrentModificationException。解决所有对共享列表的读写都包在synchronized (players)里。遍历时如果要删除元素用迭代器的remove或先复制一份再遍历。这个坑在多线程里非常经典不加锁迟早出事。4.4 现象胜负判定偶尔漏判明明五连了却说没赢原因是方向扫描写错。最常见的是反方向用了row dr * i而不是row - dr * i导致只往一边数。还有一种是把count 5写成count 5六连的时候反而不判赢。解决四个方向正反都扫count初始为 1两边累加最后 5即赢。写完拿几个边界用例测正好五连、六连、边角五连、斜向五连。4.5 现象手机连不上服务端一直提示连接超时原因按概率排一是服务端没启动或端口写错二是手机和电脑不在同一网段三是电脑防火墙拦截四是服务端绑定了127.0.0.1而不是0.0.0.0只监听本机。解决服务端new ServerSocket(8888)默认绑所有网卡如果写成new ServerSocket(8888, 50, InetAddress.getByName(127.0.0.1))就只监听本机。先确认服务端启动日志再ping电脑 IP再查防火墙最后看绑定地址。这个顺序能覆盖九成连不上的情况。5. 把毕设做出区分度从能跑到能讲清楚设计5.1 用状态机管理游戏流程能跑通的五子棋和能拿高分的五子棋差别往往在“流程是否清晰”。我一般会引入一个简单的状态机把游戏分成WAITING、PLAYING、FINISHED三个状态。服务端只接受当前状态允许的消息比如WAITING状态下收到MOVE直接丢弃。public enum GameState { WAITING, // 等待玩家 PLAYING, // 对局中 FINISHED // 已结束 } private GameState state GameState.WAITING; public void handleMove(int row, int col, int color) { if (state ! GameState.PLAYING) { return; // 非对局状态忽略落子 } // ... 落子逻辑 if (checkWin(row, col, color)) { state GameState.FINISHED; broadcast(WIN| color); } }状态机的好处是答辩时你能画一张状态转换图讲清楚每个状态下允许什么操作。这比“代码里到处 if 判断”显得专业得多而且实际维护时也少很多边界 bug。5.2 用日志和抓包验证通信正确性网络程序出问题光看代码很难定位。我的习惯是服务端每次收到和转发消息都打一行日志带时间戳和玩家标识。private static final SimpleDateFormat SDF new SimpleDateFormat(HH:mm:ss.SSS); private void log(String direction, String msg) { System.out.println(SDF.format(new Date()) [ direction ] msg); }收到消息打[RECV]转发打[SEND]。两边棋盘不一致时对着日志看哪条消息丢了或重复了比盲猜快得多。再配合 Wireshark 抓包能看到 TCP 层面的实际字节流确认协议格式没写错。5.3 文档怎么写才不像凑字数毕设文档最容易写成代码注释的堆砌。我的经验是按“问题—方案—取舍”来写每一章。比如通信协议这节先写“为什么不用现成 RPC 框架”再写“自定义文本协议的设计”最后写“这种方案的局限是什么”。有取舍的文档才像工程决策而不是说明书。文档章节写什么避免什么需求分析对战、聊天、断线重连三个核心场景泛泛而谈“用户体验好”系统设计分层图、协议表、状态机只贴类图不解释为什么这么分关键实现胜负算法、并发处理、消息转发大段贴代码无分析测试边界用例、并发用例、异常用例只写“测试通过”这张表是我带过几届毕设后总结的按这个结构写答辩时老师问哪块你都有话说。测试章节尤其重要很多人忽略但老师最爱问“你怎么保证多线程下不出错”有测试用例就能直接答。5.4 一个具体技巧用心跳检测处理断线网络对战最尴尬的是对手掉线了这边还在等下。加一个简单心跳客户端每 5 秒发一个PING服务端超过 15 秒没收到就判定掉线广播OPPONENT_LEFT。// 服务端记录每个玩家最后活跃时间 private long lastActive System.currentTimeMillis(); // 收到任何消息都更新 public void onMessage(String msg) { lastActive System.currentTimeMillis(); // ... 处理消息 } // 单独一个线程定期检查 new Thread(() - { while (true) { try { Thread.sleep(5000); } catch (InterruptedException e) { break; } long now System.currentTimeMillis(); synchronized (players) { for (ClientHandler p : players) { if (now - p.lastActive 15000) { p.close(); broadcast(OPPONENT_LEFT); } } } } }).start();5000是检查间隔15000是超时阈值一般设成心跳间隔的三倍。这个功能代码量不大但答辩时是个加分项说明你考虑了真实网络环境下的异常情况。我自己做这类项目最大的教训是别一上来就想着加功能先把“两个客户端能同步下一盘完整的棋”跑通再考虑聊天、悔棋、断线重连。很多同学卡在第一步就是因为贪多界面还没画好就去搞 AI 算法最后哪块都不完整。先把最小闭环跑通剩下的都是增量。希望帮到你。本文还有配套的精品资源点击获取