Node.js基于Socket.io构建多人在线五子棋实战

作者:终端旅客时间:2026-10-22 15:00:57浏览:650

本文详解如何利用Node.js与Socket.io模块搭建多人在线五子棋对战平台。文章深入解析了客户端与服务端通过WebSocket建立的实时通信机制,重点阐述了基于iframe伪页面跳转以维持Socket连接的技术方案,以及玩家匹配、棋局状态同步和胜负判定的后端逻辑实现。

项目概述与架构

项目源码已托管至GitHub,具体地址见原文链接。

客户端采用基础HTML5与JavaScript构建棋盘界面,服务端则依赖Node.js的Socket.io模块处理前后端数据交互。所有棋子落点记录及胜负判定逻辑均部署在服务器端执行,以确保游戏状态的一致性。

五子棋用户界面效果请参考相关展示图。

客户端界面实现较为直观,具备HTML5基础即可查阅源码理解。鉴于本文核心在于探讨Socket.io的技术应用,此处仅简要概述客户端与服务器端基于TCP连接进行数据交互的关键流程。

首先展示项目的目录结构,以便了解文件组织方式。


| server.js (socket服务器) 
| gobang-ui.html (是玩家下棋页面) 
| index.html (是用户登陆界面)
| home.html (是用户大厅界面, 用来匹配等待的 如果在线人数少于2人, 则匹配失败, 并会返回错误信息)
| game.html (client端程序的入口,内嵌iframe来显示各个页面,通过改变iframe的src属性,来达成伪页面跳转)
| img (图片资源文件夹)
| tou.jpg (棋盘界面用户的头像,因为登录界面只要输入用户名就可以开始游戏了,所以所有用户的头像都是一样的)

客户端界面与连接维持机制

game.html作为主界面文件,负责承载整个游戏逻辑。


<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
 <style>
 * {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
 }
 </style>
 <!-- 引入cdn上的socket.io库 -->
 <script src="https://cdn.bootcss.com/socket.io/2.1.0/socket.io.js"></script>
</head>
<body>
<!-- 这里是程序的入口,通过js改变src属性,来切换页面 -->
<iframe id="game" src="index.html" width="100%" height="100%" scrolling="no"></iframe>
</body>
</html>

为何采用嵌入iframe并动态修改src属性的方式来模拟页面跳转?因为常规的页面刷新或跳转会导致HTTP请求重发,进而强制断开现有的Socket连接。为避免这种连接中断现象,我们利用iframe的src属性切换来模拟页面导航,从而保持Socket长连接的稳定性。


// 这行代码表示client端对server端进行第一次连接
var socket = io('ws://localhost:3000')

登录流程与Socket通信

index.html为用户登录入口页面。


<!-- 这是用户登录的按钮 -->
<div onclick="login()">开始游戏</div>

当用户点击按钮时,触发JavaScript中的login方法。该方法并不直接建立与服务器的连接,因为Socket连接已在game.html中初始化。当前页面实质上是game.html的子页面,因此login方法在校验输入框内容非空后,通过全局对象parent调用父页面(game.html)中定义的login方法。


// index.html中的login方法
function login() {
 if (username.value === undefined || username.value === '') {
  return
 }
 // 调用父窗口的login方法
 parent.login(username.value)
}

在game.html中定义的login方法,负责通过Socket向服务器发送login事件。


function login(username) {
 socket.emit('login', username)
}

服务端逻辑与玩家匹配

server.js文件包含了服务器端的核心逻辑代码。


// 监听连接
io.on('connection', function (socket) {
 // 玩家登陆, socket.emit('login', username)就是触发了这个事件
 // 监听了login事件
 socket.on('login', function (name) {
 // players是一个全局数组,里面存放了所有的玩家对象,如果players中 
 var flag = players.some(function (value) {
  return value.name === name
 })
 if (flag) {
  socket.emit('home', {'flag': true})
 } else {
  console.log(name + '已登陆')
  // 创建玩家
  new Player(socket, name)
  // 将玩家放进数组中
  // players.push(player)
  // 如果用户名没有重名,那么触发client端的home事件
  socket.emit('home', {'playerCount': playerCount, 'name': name})
 }
 })
})

客户端监听来自服务器的home事件,以处理登录后的状态反馈。


// 玩家登陆成功
 socket.on('home', function (data) {
 if (data.flag) {
  game.contentWindow.flag.hidden = false
 } else {
  game.contentWindow.flag.hidden = true
  // 保存用户名和玩家在线人数到localStorage中
  localStorage.setItem('name', data.name)
  localStorage.setItem('playerCount', data.playerCount)
  // location.href = './home.html'
  game.src = 'home.html'
 }
 })

home.html为玩家匹配大厅界面,其结构与index.html相似,同样包含一个匹配按钮。点击该按钮会触发play事件,发起游戏匹配请求。


// 玩家开始匹配
 this.socket.on('play', function () {
 // 如果空闲玩家总数大于或等于2,那么开始游戏
 if (playerCount >= 2) {
  self.pipei = true
  // 如果已经有人在开始匹配了,那么这个玩家就不需要走下面函数了,因为继续执行的话相当于再开一个棋局
  if (isExistFZ(self) > 0) {
  // 保持不动就好,房主会自动找到你的
  return
  }
  // 如果没有房主,那么这个玩家将成为房主
  self.fz = true
  // 可用的玩家数
  var player2 = null
  self.timer = setInterval(function () {
  console.log('正在匹配...')
  if (player2 = findPlayer(self)) {
   console.log('匹配成功')
   self.gamePlay = new Game(self, player2)
   player2.gamePlay = self.gamePlay
   clearInterval(self.timer)
  }
  }, 1000)
 } else {
  socket.emit('player less')
 }
 })

server.js中定义了两个核心类:Player(玩家类)和Game(棋局类)。一个完整的棋局实例对应两名玩家。

Player类包含以下关键属性,用于维护玩家状态。


this.socket = socket // socket对象,玩家通过它来监听数据
 this.name = name // 玩家的名称
 this.color = null // 玩家棋子的颜色
 this.state = 0 // 0代表空闲, 1在游戏中
 this.pipei = false // 是否在匹配
 this.gamePlay = null // 棋局对象
 this.flag = true // 是否轮到这个玩家出棋
 this.fz = false // 是否是房主

Player类对象监听特定事件,以响应客户端的操作指令。


// 监听玩家是否退出游戏
 this.socket.on('disconnect', function () {
  // 删除数组中的玩家
  // players.splice(players.indexOf(self), 1) // 删不掉
  // delete players[players.indexOf(self)]
  // 新的删除方式
  players = players.filter(function (value) {
   return value.name !== self.name
  })
  playerCount--
  // 如果退出游戏的玩家正在进行游戏,那么这局游戏也该退出
  if (self.state === 0) {
   gameCount--
  }
  console.log(self.name + '已退出游戏')
 })
 // 玩家开始匹配
 this.socket.on('play', function () {
  // 如果空闲玩家总数大于或等于2,那么开始游戏
  if (playerCount >= 2) {
   self.pipei = true
   // 如果已经有人在开始匹配了,那么这个玩家就不需要走下面函数了,因为继续执行的话相当于再开一个棋局
   if (isExistFZ(self) > 0) {
    // 保持不动就好,房主会自动找到你的
    return
   }
   // 如果没有房主,那么这个玩家将成为房主
   self.fz = true
   // 可用的玩家数
   var player2 = null
   self.timer = setInterval(function () {
    console.log('正在匹配...')
    if (player2 = findPlayer(self)) {
     console.log('匹配成功')
     self.gamePlay = new Game(self, player2)
     player2.gamePlay = self.gamePlay
     clearInterval(self.timer)
    }
   }, 1000)
  } else {
   socket.emit('player less')
  }
 })
 // 玩家取消匹配按钮
 this.socket.on('clearPlay', function () {
  clearInterval(self.timer)
 })
  // 监听数据,玩家下棋的时候触发
 this.socket.on('data', function (data) {
  if (self.flag) {
   add_pieces(self.gamePlay, data, self.color)
  }
 })
 // 最后将当前玩家实例放到players全局玩家数组中去
 players.push(this)

Game类负责管理具体的棋局逻辑。


// 棋盘的格子数
  this.column = 21
  this.arr = init_arr() // 存储棋盘坐标的二位数组
  // 一局棋局上的两个玩家
  this.play1 = play1
  this.play2 = play2
  // 修改游戏状态
   this.play1.state = 1
   this.play2.state = 1
   // 在游戏中,是否匹配为false
   this.play1.pipei = false
   this.play2.pipei = false
   this.play1.fz = false
   this.play1.fz = false
   // 随机给两个玩家分配棋子颜色
   this.play1.color = ~~(Math.random() * 2) === 0 ? 'white' : 'black'
   this.play2.color = this.play1.color === 'white' ? 'black' : 'white'
   // 谁是白棋谁先走
   this.play1.flag = this.play1.color === 'white'? true: false
   this.play2.flag = this.play2.color === 'white'? true: false

添加棋子方法实现了棋子放置及棋盘状态的更新。


// 添加棋子
function add_pieces(self, position, color) {
 if (self.arr[position.x][position.y] === undefined) {
  self.arr[position.x][position.y] = color
  if (color === self.play1.color) {
   self.play1.flag = false
   self.play2.flag = true
  } else if (color === self.play2.color) {
   self.play1.flag = true
   self.play2.flag = false
  }
  check_result(self, self.arr, position, color)
 }
}
// 初始化数组
function init_arr() {
 var arr = []
 for (var i = 0; i < 21; i++) {
  arr.push(new Array(21))
 }
 return arr
}

欢迎在GitHub上下载完整源码以供学习参考。如有任何疑问,欢迎留言交流,作者将尽力解答。感谢各位对本站的支持。

作者最新文章