HTML5游戏之Websocket俄罗斯方块基础版(一)
俄罗斯方块游戏,对于我们这个年龄段的小伙伴来说都是童年的美好回忆了,这可是当年的经典啊!呵呵,我不是来暴露年龄的。当然现在发展快了,各种小游戏层出不穷,很多年轻的孩子已经对俄罗斯方块不感兴趣了,甚至都没接触过,之所以把它作为我们今天实现的案例,希望对大家在脑海里有一个初步的认知,毕竟玩过的都知道它的具体样子,方便大家理解,没玩过的也没有关系,慢慢跟着我一步一步学,满足你的要求滴。本篇俄罗斯方块游戏是基于 HTML5 的 Websocket 实现的,主要带领大家了解要实现俄罗斯方块的基础知识 WebSocket,以及 socket.io,为后续实现俄罗斯打下基础。本 demo 案例共分为三章来学完,第二章请移步到《HTML5 游戏之 Websocket 俄罗斯方块进阶版(二)》观看。
简单介绍一下这个游戏,首先它是双人一起来玩的,玩家可以通过上下左右空格键操作我的游戏区域的俄罗斯方块(图一),对方玩家游戏区域界面同样也有这样的操作(图二),那么对方玩家游戏区域的操作会被实时的同步到我的游戏区域界面的对方的游戏区域(图一),这样我在玩俄罗斯方块的时候就可以实时了解到对方玩家的游戏情况,从而进行一个比赛。
俄罗斯方块介绍图一:
俄罗斯方块介绍图二:
为了完成这个案例,我们需要小伙伴们具备以下知识:
1、一些 HTML 和 CSS 的基础知识;
2、javascript 基础知识;
3、用过 node.js,因为我们的server
是用 node 搭建的。
希望大家先把 node.js 下载下来安装在自己的电脑上,然后运行一个 helloworld 的例子,由于篇幅内容比较长,所以为了大家学习更轻松,我将本课程分为三篇文章来学习,分别是基础班,单机版,升级版。在本篇我会给大家介绍websocket
基础知识、以及 websocket 的相关例子。
Websocket 初体验
websocket
是伴随 HTML5 出现的一门新技术,它和传统的http
是不同的,我们知道传统的 http 请求是由浏览器发起,服务端接收到请求后返回一个数据,那么,这样一次来回之后请求就断了。但是websocket
不一样,同样是由浏览器发出一个请求,但这个请求呢是websocket
请求,当服务器接收到一个websocket
请求的时候,它会在服务器和浏览器之间建立一个 web 端和socket
链接,这个socket
链接就允许浏览器和服务器相互发送消息,如果浏览器端和服务器端都不断开的话,这个 socket 链接是不会放开的,本质就是 TCP 的链接。我们看一下具体代码
HTML 代码:
<h1>Echo test</h1> <input id="sendTxt" type="text"/> <button id="sendBtn">发送</button> <div id="recv"></div>
JS 代码:
var websocket = new WebSocket("ws://echo.websocket.org/"); websocket.onopen = function () { console.log('wbsocket open'); document.getElementById("recv").innerHTML = "Connected"; } websocket.onclose = function () { console.log('websocket close'); } websocket.onmessage = function (e) { console.log(e.data); document.getElementById("recv").innerHTML = e.data; } document.getElementById("sendBtn").onclick = function () { var txt = document.getElementById("sendTxt").value; websocket.send(txt); }
注意 newWebSocket 的大小写,ws 为 websocket 的协议,不是 http。
看一下效果:
上图我们看到 websocket 已经成功连接,在 input 框中随便输入写东西,数据也能被成功的接收到,到这儿,相信大家明白案例是什么意思了,当我们在输入框写一些东西,数据发送到那儿了呢,它其实就是发送给了”ws://echo.websocket.org/
“这个地址,这个地址的背后就是websocket
的server
,server
是将你发送过去的数据原分不动的返回给你,在图上可以看出接收到的就是返回到的数据。
接着我们打开控制台(F12 或者右键“审查元素”),选择network
,勾选上filter
,然后我们选择WS
,刷新一下页面就看到 websocket 发出了请求。
我们看一下 Headers 里面,Connection
它是Upgrade
,Upgrade
字段是websocket
。在RequestHeaders
里面呢,Connection
也是Upgrade
,Upgrade
字段也是websocket
。这些都是 websocket 独有的特征。
我们在切换到 Frames 里面,我们可以看到 websocket 发送和接收到的数据,假如我在 input 框输入数据,从 frames 中我们看到,绿色条代表我发送出去的数据,白色条代表我接收到的数据。
以上内容就是 websocket 在浏览器的一些基础知识。在这个例子中我们服务器用到的 ws 地址是”ws://echo.websocket.org/”显然是别人的server
,接下来我们用 node 来搭建一个自己的 WebSocket 的 server 服务器。
搭建自己的 websocket server
在实现 websocket server 前,我们需要一个模块叫 nodejs-websocket,在 GitHub 上找到nodejs-websocket项目地址,在终端工具上通过命令 npm install nodejs-websocket 在我们当前的项目中安装该模块。
安装成功之后,我们还需要一段代码
var ws = require("nodejs-websocket") // Scream server example: "hi" -> "HI!!!" var server = ws.createServer(function (conn) { console.log("New connection") conn.on("text", function (str) { console.log("Received "+str) conn.sendText(str.toUpperCase()+"!!!") }) conn.on("close", function (code, reason) { console.log("Connection closed") }) }).listen(8001)
简单看一下代码,首先将node-websocket
模块引进来,然后调用createServer
,里面有一个参数是回调函数,这个 server 的 listen 端口是 8001,当客户端发送链接过来的时候,coon 参数就代表这个链接,text 当客户端有消息发过来的时候就被放到 str 里面,然后通过toUpperCase
方法将 str 变成大写的,然后在链接 close 关闭的时候打印出一段话。
我们通过命令node wsServer.js
运行起来,然后修改”ws://echo.websocket.org/
“地址为”ws://localhost:8001/”,我们在页面上测试一下,我在 input 框输入 hi,可以看到打印出来的是“HI!!!”
然后在终端它也打印出了相应的日志
但是这个程序有个小小的毛病,比如说我现在把运行窗口关闭后,在终端显示 server 挂了,告诉我们有一个未处理的 error 事件,如何解决呢?
我们在 GitHub 上的 node-websocket 官网上查找一下看有没有相关方法,果不其然。
我们打开我们的 js,在 close 后边加一个 error 处理方法,然后我们将端口号拿出来定义一下,传入到 listen,在每次启动时打印一句话,代码修改如下。
var ws = require("nodejs-websocket") var PORT = 3000 // Scream server example: "hi" -> "HI!!!" var server = ws.createServer(function (conn) { console.log("New connection") conn.on("text", function (str) { console.log("Received "+str) conn.sendText(str.toUpperCase()+"!!!") }) conn.on("close", function (code, reason) { console.log("Connection closed") }) coon.on("error", function (err) { console.log("handle err"); console.log(err) }) }).listen(PORT) console.log("websocket server listening on port " + PORT)
这样每次启动 server 的时候都会打印我们设置好的话
这样我们通过对代码的修改,无论输入,还是关闭窗口,都不会出现出现错误,error 也被我们打印出来了。
实现简单聊天功能
HTML 代码部分:
<h1>Chat Room</h1> <input id="sendTxt" type="text"/> <button id="sendBtn">发送</button>
JS 代码部分:
var websocket = new WebSocket("ws://localhost:3000/"); function showMessage(str) { var div = document.createElement('div'); div.innerHTML = str; document.body.appendChild(div); } websocket.onopen = function () { console.log('wbsocket open'); document.getElementById("sendBtn").onclick = function () { var txt = document.getElementById("sendTxt").value; if(txt) { websocket.send(txt); } } } websocket.onclose = function () { console.log('websocket close'); } websocket.onmessage = function (e) { console.log(e.data); showMessage(e.data); }
当我们点击发送按钮的时候,它会把我们文本框内的内容发送给 server,当我们接收到数据的时候,会把这个数据变成一个 div 添加到我们的 body 里面。
接下来我们对 server 进行改造,首先,我们不可能有一个客户端来连接,我们需要给每个客户端分开一个名字,所以我定义了一个客户端的计数器clientCount
,然后在每一次连接的时候自增,然后我们给conn
附一个变量,所以当第一个用户连上之后就是 user1,第二个就是 user2,以此类推。进来之后给每一个客户端发送一个消息通知他们这个客户端进来了,这里我们定义一个 broadcast 函数
如何实现这个函数呢?我们只要取到所有 server 下面所有连接,然后遍历每个连接跟每一个连接都去调用 sendText 方法就可以了。
那么怎么样取到 server 下的所有链接呢?在 server 有一个connections
这么个东西,在这个变量里面就保存了所有连接,在调用forEach
,每一个链接就叫connection
,然后每一个连接去sendText
,这样我们就实现了广播的方法。
var ws = require("nodejs-websocket") var PORT = 3000; var clientCount = 0; // Scream server example: "hi" -> "HI!!!" var server = ws.createServer(function (conn) { console.log("New connection") clientCount ++; conn.nickname = 'user' + clientCount; broadcast(conn.nickname + ' comes in') conn.on("text", function (str) { console.log("Received "+str) broadcast(str) }) conn.on("close", function (code, reason) { console.log("Connection closed") broadcast(conn.nickname + ' left') }) conn.on("error", function (err) { console.log("handle err"); console.log(err) }) }).listen(PORT) console.log("websocket server listening on port " + PORT); function broadcast(str) { server.connections.forEach(function(connection){ connection.sendText(str) }) }
虽然功能实现了,但是还不是很完美,我们在基础上再去优化一下。现在这个e.data
知识消息本身,但是我们想想一个用户进来是一个进入的消息,一个用户离开是一个离开的消息,所以作为消息除了本身之外还有一个消息属性,即消息应该是作为一个对象来传的,而在我们的onmessage
里的e.data
只能接收字符串,那么我们怎么样把一个对象传递过来呢?
我们在 server 中第一次获取消息的地方定义一个 mes 对象,然后我们设置 mes 对象的 type 属性为“enter”表示进入的消息,它的数据就是我们发送的这个字符串“conn.nickname+'comesin'
”,broadcast 发什么,我们可以把对象利用 JSON.stringify 方法给它格式化一下变成字符串,同样的道理,在这个 text 中给我们也进行这样的改造,type 就是 message,data 就是我们的字符串 str。然后我们再对 close 进行改造,type 为 leave,data 为“conn.nickname+'left'
”这样我们的所有 server 就改造完毕了。
var ws = require("nodejs-websocket") var PORT = 3000; var clientCount = 0; // Scream server example: "hi" -> "HI!!!" var server = ws.createServer(function (conn) { console.log("New connection") clientCount ++; conn.nickname = 'user' + clientCount; var mes = {} mes.type = "enter" mes.data = conn.nickname + ' comes in' broadcast(JSON.stringify(mes)) conn.on("text", function (str) { console.log("Received "+str) var mes = {} mes.type = "message" mes.data = str broadcast(JSON.stringify(mes)) }) conn.on("close", function (code, reason) { console.log("Connection closed") var mes = {} mes.type = "leave" mes.data = conn.nickname + ' left' broadcast(JSON.stringify(mes)) }) conn.on("error", function (err) { console.log("handle err"); console.log(err) }) }).listen(PORT) console.log("websocket server listening on port " + PORT); function broadcast(str) { server.connections.forEach(function(connection){ connection.sendText(str) }) }
index 页面内也需要改造,主要是对onmessage
我们接收到消息的时候,e.data
已经是被格式化的 json 了,我们可以定义一个对象 mes,利用JSON.parse
把这个格式化的字符串再变回一个对象,然后在showMessage
传递mes.data
和mes.type
,然后在 showMessage 在传入一个 type 参数,在里面判断一下 type 如果是 enter 进入的消息,把他的颜色变为蓝色,如果是 leave 离开的消息,把它变为红色,这样我们通过 div 的颜色来区分消息的类型。
var websocket = new WebSocket("ws://localhost:3000/"); function showMessage(str, type) { var div = document.createElement('div'); div.innerHTML = str; if(type == "enter"){ div.style.color = "blue"; }else if (type == "leave"){ div.style.color = "red" } document.body.appendChild(div); } websocket.onopen = function () { console.log('wbsocket open'); document.getElementById("sendBtn").onclick = function () { var txt = document.getElementById("sendTxt").value; if(txt) { websocket.send(txt); } } } websocket.onclose = function () { console.log('websocket close'); } websocket.onmessage = function (e) { console.log(e.data); var mes = JSON.parse(e.data); showMessage(mes.data, mes.type); }
还有一个需要我们解决的问题是,我们不能判断是谁发送的消息,这个很简单在我们聊天消息的地方 message,把它的名字加上即可“conn.nickname+'says:'+str
”。
var ws = require("nodejs-websocket") var PORT = 3000; var clientCount = 0; // Scream server example: "hi" -> "HI!!!" var server = ws.createServer(function (conn) { console.log("New connection") clientCount ++; conn.nickname = 'user' + clientCount; var mes = {} mes.type = "enter" mes.data = conn.nickname + ' comes in' broadcast(JSON.stringify(mes)) conn.on("text", function (str) { console.log("Received "+str) var mes = {} mes.type = "message" mes.data = conn.nickname + ' says: ' + str broadcast(JSON.stringify(mes)) }) conn.on("close", function (code, reason) { console.log("Connection closed") var mes = {} mes.type = "leave" mes.data = conn.nickname + ' left' broadcast(JSON.stringify(mes)) }) conn.on("error", function (err) { console.log("handle err"); console.log(err) }) }).listen(PORT) console.log("websocket server listening on port " + PORT); function broadcast(str) { server.connections.forEach(function(connection){ connection.sendText(str) }) }
到这里一个简单的聊天功能就实现了,但是回头看一下我们的代码,还有一些问题,比如在我们的代码中出现大量类似于下面的这种代码:
var mes = {} mes.type = "message" mes.data = conn.nickname + ' says: ' + str broadcast(JSON.stringify(mes))
这种代码就是为了把一个对象发送过去而进行的一些格式化,在 index 页面里呢又把这个字符串反格式化变成一个对象,然后再根据这个对象里面的某个属性比如说类型,根据这个类型进行判断去做相应的业务逻辑。其实这个过程呢和我们的业务逻辑是无关的,什么意思呢,就是现在我们的聊天室需要去做,而俄罗斯方块也需要做这个过程,之后的这个 websocket 通信可能都需要做这个过程,这种过程就不需要我们来做了,应该交给框架去做,而这种框架早就被人实现好了,所以接下来我会带大家通过 socket.io 来实现这一通信过程,然后大家和我们自己实现的通信过程做个对比看看有什么不同。
socket.io 入门
打开 socket.io 官网,通过指令下载安装
npm install socket.io
将 server 代码放入到我们新建好的 server.js
var app = require('http').createServer() var io = require('socket.io')(app); app.listen(3000); io.on('connection', function (socket) { socket.emit('news', { hello: 'world' }); socket.on('my other event', function (data) { console.log(data); }); });
index.html 内容,引入 socket.io.js(建议下载到本地引入)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Websocket</title> <script src="socket.io.js"></script> </head> <body> <script> var socket = io('ws://localhost:3000'); socket.on('news', function (data) { console.log(data); socket.emit('my other event', { my: 'data' }); }); </script> </body> </html>
然后我们运行一下,它的结果是在控制台输出,里面有一个“hello:world
”
在我们的而服务器端有一个my:'data'
我们简单看一下上面代码,在 server 中 on 中有一个function
函数里面有一个 socket,代表和客户端之间的连接,主要看一下 emit,emit 就是发送数据,里面有一个参数 news,第二个参数直接就是一个对象,那么,在 index 页面上是怎么接收的呢?直接就是socket.on
里面一个 function,然后数据 data 直接过来,emit 的发送和服务器上的一模一样。
socketio 主要给我们带来两个方面的优势,一是可以直接发送对象,不需要我们之前那样将对象变成字符串接收到之后再把这个字符串变成一个对象;二是大家可以看到’my other event’显然是我们自定义的一个事件,也就是说它这个socket.on
任何东西,这样就不需要我们在对象中去定一个类似 type 的东西。这就是socketio
给我们带来的两个好处。
socket.io 改造聊天功能
index.html 代码:
<head> <meta charset="UTF-8"> <title>Websocket</title> <script src="socket.io.js"></script> </head> <body> <h1>Chat Room</h1> <input id="sendTxt" type="text"/> <button id="sendBtn">发送</button> <script> var socket = io('ws://localhost:3000/'); function showMessage(str, type) { var div = document.createElement('div'); div.innerHTML = str; if(type == "enter"){ div.style.color = "blue"; }else if (type == "leave"){ div.style.color = "red" } document.body.appendChild(div); } document.getElementById("sendBtn").onclick = function () { var txt = document.getElementById("sendTxt").value; if(txt) { socket.emit('message', txt); } } socket.on('enter', function (data) { showMessage(data, 'enter'); }) socket.on('message', function (data) { showMessage(data, 'message'); }) socket.on('leave', function (data) { showMessage(data, 'leave'); }) </script> </body>
server 代码部分:
var app = require('http').createServer() var io = require('socket.io')(app); var PORT = 3000; var clientCount = 0; app.listen(3000); io.on('connection', function (socket) { clientCount ++; socket.nickname = 'user' + clientCount; io.emit('enter',socket.nickname + ' comes in') socket.on('message', function (str) { io.emit('message', socket.nickname + ' says: ' + str) }) socket.on('disconnect', function () { io.emit('leave', socket.nickname + ' left') }) }); console.log("websocket server listening on port " + PORT);
效果展示:
使用socket.io
改造代码,可以看出比之前我们写的代码经济了许多,不再那么臃肿。源码下载websocket
结束语
以上所有知识点就为大家介绍完了,给大家再来一下小小的总结, 首先 websocket 特点是允许浏览器和服务器建立持久的连接,如果你的业务中需要服务器向浏览器发送数据的场景呢,使用 websocket 是再合适不过了;接下来又为大家介绍了 HTML5 的 websocket API,主要包括on.open
,on.message
,on.close
;然后又给大家介绍了怎样使用nodejs-websocket
去实现一个简单的websocket server
;然后我们利用这两个知识点去实现一个简单的聊天功能,但是 nodejs-websocket 模块功能不够强大,所以又给大家介绍 socket.io 这个模块,它主要有两个方面的优势,第一,在发送数据的时候,可以直接发送 js 对象,这样就不需要在发送数据前对对象进行格式化,然后在收到数据之后呢,又把格式话后的字符串反序列化成一个对象,省去了这些步骤;第二,它可以去自定义消息,这样就不需要在数据里面设计 type 这样一个字段,最后利用 socket.io 把我们之前写的聊天功能改造了一下,代码精简了许多,希望大家下去之后对 websocket 多多联系,熟练掌握,在接下来的俄罗斯方块案例它的背后通讯原理都是根据本章介绍的 websocket。如有问题,留言探讨,语句不当之处请多多包涵!
码云笔记 » HTML5游戏之Websocket俄罗斯方块基础版(一)