WebSocket
什么是 WebSocket
WebSocket协议是基于 TCP
的一种新的 网络协议
。
它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
WebSocket 和 HTTP 的区别
HTTP协议的特点:
1、服务器只能响应客户端的请求,不能主动向客户端推送数据
2、客户端的每次请求都需要连接、断开,即每次请求都是一个全新的请求
WebSocket的特点:
1、客户端与服务器端在连接时可以互相推据数据
2、客户端连接到服务器之后,会一直保持连接的状态,直到有一端主动断开连接
应用范围
WebSocket 一般用来开发聊天室
、网页游戏
等需要长连接并且可以双向通信的应用程序。
PHP 实现 WebSocket
在 PHP 中实现 WebSocket
可以使用 Swoole
或者 Workerman
。
Swoole 和 Workerman 是两个 PHP 中使用的多进程 socket 服务器框架,支持HTTP、WebSocket、Tcp、Udp等多种协议。
Swoole:是用 C 语言开发的 PHP 扩展,只能在 Linux 上使用,性能更好、上手有点难度。
Workerman:是用 PHP 开发的框架,windows 和 Linux 上都可以使用,无须安装下载即可使用、性能相对swoole弱一些,上手比较简单。
Workerman
官方文档:http://doc.workerman.net/
下载
Workerman 无须安装,下载、引入之后即可使用。
下载地址:https://www.workerman.net/download/workermanzip
引入
下载之后直接引入使用:
<?php
use Workerman\Worker;
require_once '下载目录/Workerman-master/Autoloader.php';
// 实例化 Worker 类对象
$worker = new Worker('websocket://0.0.0.0:8686');
核心类
workerman 中最核心的两个类是 Worker
和 TcpConnection
类。
Worker:端口监听、启动服务、接收请求等。
TcpConnection:负责管理每个连接的客户端,接收客户端信息,发送客户端信息等。
Worker
Worker用来绑定端口、启动服务器。
- 构造函数
创建 Worker 时需要指定协议、IP和端口号。
比如:启动 websocket 服务器,绑定到 8686 端口(1~65535,1000以 内的端口号留给系统):
$worker = new Worker('websocket://0.0.0.0:8686');
- 重要属性
connections:保存所有连接的客户端对象(TcpConnection类对象)的数组。
count:启动的进程数,默认是1(我们现在先只启动1个,启动多会出现多进程的问题)。
- 回调函数
onWorkerStart:当子进程启动时触发的函数,一般做一些初始化的功能,连接数据库等。
onConnect:当有客户端连接成功时触发的函数。
onMessage:当收到客户端的数据时触发的函数。
onClose:当与客户端断开连接时触发的函数。
- 接口
runAll:启动程序。
示例代码:
server.php
<?php
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 启动时调用
function start($worker)
{
echo '启动成功!';
}
// 有连接时调用
function connect($connection)
{
echo '有客户端连接~';
}
// 当收到数据时调用
function message($connection, $data)
{
echo '收到消息'.$data;
}
// 当有客户端断开连接时调用
function close($connection)
{
echo '有客户端断开连接~';
}
// 绑定端口
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数为1
$worker->count = 1;
// 设置回调函数
$worker->onWorkerStart = 'start';
$worker->onConnect = 'connect';
$worker->onMessage = 'message';
$worker->onClose = 'close';
// 启动
Worker::runAll();
TcpConnection
每个客户端的连接就是一个 TcpConnection
类的对象,所以上面代码中的 $connection
就是 TcpConnection
这个类的对象,通过这个对象我们就可以获取客户端的信息,也可以向客户端发送数据。
- 重要属性
id:每个客户端都有一个 id,它是一个自增的整数,可以通过这个 id 来区别客户端。
worker:当前这个连接所属的 worker 对象。
- 接口
send:向客户端发送数据。
close:关闭与这个客户端的连接。
示例代码、当收到消息时,把消息转发给所有其它的客户端:
<?php
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 启动时调用
function start($worker)
{
echo '启动成功!';
}
// 有连接时调用
function connect($connection)
{
echo '有客户端连接~';
}
// 当收到数据时调用
function message($connection, $data)
{
// 循环所有客户端
foreach($connection->worker->connections as $c)
{
// 如果不是自己就转发消息
if($connection->id != $c->id)
{
$c->send($data);
}
}
}
// 当有客户端断开连接时调用
function close($connection)
{
echo '有客户端断开连接~';
}
// 绑定端口
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数为1
$worker->count = 1;
// 设置回调函数
$worker->onWorkerStart = 'start';
$worker->onConnect = 'connect';
$worker->onMessage = 'message';
$worker->onClose = 'close';
// 启动
Worker::runAll();
启动、关闭
与之前学习的程序不同 workerman 的程序是在命令行中运行的。
可以使用以下指令启动、关闭程序:
php server.php start // 启动
php server.php stop // 关闭程序
示例、启动( server.php 为脚本的名字)
客户端
使用 PHP 启动了 WebSocket 服务器之后,会在服务器上监听一个端口号(上面代码中的 8686)。有了服务器之后,我们就可以编写前端代码连接服务器进行数据通信。
客户端可以是任何一个支持 WebSocket
协议的软件,我们常用的就是浏览器。
在浏览器中编写代码自然是使用 JavaScript
语言了。
JS 中的 WebSocket
使用 JavaScript 实现 WebSocket 非常的简单,只需要以下几步:
1、连接服务器
2、绑定回调函数接收、处理数据
3、调用 send 方法发送数据
连接服务器
JS 中提供了一个 WebSocket 类,可以通过 new 来连接服务器,连接时需要指定服务器的IP地址和端口号:
var ws = new WebSocket('ws://ip:端口号');
绑定回调函数
连接了服务器之后,我们需要绑定几个回调函数,来处理相应的事件:
onopen:当连接成功时触发。
onmessage:当收到消息时触发。
onclose:当与服务器断开连接时触发。
// 连接服务器成功时触发
ws.onopen = function() {
console.log("连接成功")
}
// 收到服务器消息时触发
ws.onmessage = function(e) {
console.log("收到服务端的消息:" + e.data)
}
// 与服务器断开连接时触发
ws.onclose = function(e) {
console.log("与服务器断开连接")
}
方法
可以调用 send
方法向服务器发送数据,也可以调用 close
方法断开和服务器的连接:
ws.send('Hello Tom') // 向服务器发送数据
ws.close(); // 断开连接
示例代码:
client.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket 客户端</title>
</head>
<body>
</body>
</html>
<script>
ws = new WebSocket("ws://127.0.0.1:8686")
ws.onopen = function() {
console.log("连接成功")
ws.send('tom')
console.log("给服务端发送一个字符串:tom")
}
ws.onmessage = function(e) {
console.log("收到服务端的消息:" + e.data)
ws.close();
}
ws.onclose = function(e) {
console.log("与服务器断开连接")
}
</script>
前、后端图解:
连接时传递参数
有时,我们希望在客户端连接服务器时就传递一些参数,这时可以这样做:
客户端在连接时通过 GET 方式传递参数:
ws = new WebSocket('ws://127.0.0.1:8686?name=tom&age=10')
服务器端在连接时接收参数:
$worker-onConnect=function($connection)
{
$connection->onWebSocketConnect = function ($connection, $http_header) {
// 使用 $_GET 接收参数:
echo $_GET['name'];
echo $_GET['age'];
};
}
案例、聊天室:群聊天
核心思路:使用 Worker 类的 connections 属性可以得到保存所有客户端 $connection 对象的数组,然后我们可以循环这个数组调用每个客户端的 send 方法给每个客户端发消息。
服务器端:server.php
<?php
/*
WebSocket 的服务器端
*/
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 实例化 Worker 类对象
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数
$worker->count = 1;
// 设置回调函数
// 绑定连接的回调函数,这个函数会在有客户端连接时调用
// 参数:TcpConnection 类的对象,代表每个客户端
$worker->onConnect = function( $connection ) {
// 向这个客户端发数据
$connection->send('欢迎您~');
};
// 接收消息
$worker->onMessage = function($connection, $data) {
global $worker;
// 循环所有的客户端,给它们发消息
foreach($worker->connections as $c)
{
$c->send($data);
}
};
// 运行
Worker::runAll();
客户端:index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="(v,k) in messages">
有人说:{{v}}
</li>
</ul>
<textarea v-model="content"></textarea>
<input @click="submit" type="button" value="发送">
</div>
</body>
</html>
<script src="vue.min.js"></script>
<script>
new Vue({
el:'#app',
data: {
ws: null, // 保存 WebSocket 对象
content:'',
messages: [] // 保存所有接收的消息
},
// 当 vue 创建时调用
created: function(){
this.ws = new WebSocket('ws://127.0.0.1:8686')
this.ws.onopen = this.open
this.ws.onmessage = this.message
},
methods:{
submit:function(){
// 把框里的内容发送到服务器
this.ws.send( this.content )
// 清空框
this.content = ''
},
open: function() {
alert('连接成功!')
},
message: function(e) {
// 把接收到的消息放到页面中(e.data 就是接收的数据)
this.messages.push(e.data)
}
}
})
</script>
案例、聊天室:频道聊天
频道列表页:index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
li{
padding: 5px;
list-style-type: none;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="app">
<h3>房间列表</h3>
<ul>
<li v-for="(v,k) in rooms">
<p><a :href="'room.html?id=' + v.id">{{v.title}}</a></p>
<p>主播:{{v.username}}</p>
</li>
</ul>
</div>
</body>
</html>
<script src="vue.min.js"></script>
<script>
new Vue({
el:'#app',
data: {
rooms: [
{
id: 1,
title: '世界杯',
username: '大灰狼'
},
{
id: 2,
title: 'PHP',
username: '大雷'
}
]
}
})
</script>
频道聊天室:room.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="(v,k) in messages">
有人说:{{v}}
</li>
</ul>
<textarea v-model="content"></textarea>
<input @click="submit" type="button" value="发送">
</div>
</body>
</html>
<script src="vue.min.js"></script>
<script>
new Vue({
el:'#app',
data: {
ws: null, // 保存 WebSocket 对象
content:'',
messages: [] // 保存所有接收的消息
},
// 当 vue 创建时调用
created: function(){
let get = GetRequest()
// 获取 id
this.ws = new WebSocket('ws://127.0.0.1:8686?room_id='+get.id)
this.ws.onopen = this.open
this.ws.onmessage = this.message
},
methods:{
submit:function(){
// 把框里的内容发送到服务器
this.ws.send( this.content )
// 清空框
this.content = ''
},
open: function() {
alert('连接成功!')
},
message: function(e) {
// 把接收到的消息放到页面中(e.data 就是接收的数据)
this.messages.push(e.data)
}
}
})
// 从百度里找到的一个可以接收所有 URL 上?后的参数的函数
// 这个函数会解析 url 上的参数并返回一个对象,保存所有的参数
// 返回数据:{id:1}
function GetRequest() {
var url = location.search; //获取url中"?"符后的字串
var theRequest = new Object();
if (url.indexOf("?") != -1) {
var str = url.substr(1);
strs = str.split("&");
for(var i = 0; i < strs.length; i ++) {
theRequest[strs[i].split("=")[0]]=unescape(strs[i].split("=")[1]);
}
}
return theRequest;
}
</script>
服务器:server.php
<?php
/*
WebSocket 的服务器端
*/
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 实例化 Worker 类对象
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数
$worker->count = 1;
/*
定义一个房间的数组,保存每个房间中的客户端,这样在群发时就不需要循环所有的客户端了,
只需要循环自己房间这个数组即可(这样做会占用更多的内存,但性能会提高(内存换性能))
$rooms[
1 => [小明,三毛, '四娃']
2 => [大娃,二娃,三娃]
];
$rooms[1][] = '四娃'
结构:二维数组
下标:房间的ID
值:保存房间中所有的客户端
*/
$rooms = [];
// 绑定连接的回调函数,这个函数会在有客户端连接时调用
// 参数:TcpConnection 类的对象,代表每个客户端
$worker->onConnect = function( $connection ) {
// 为了能够使用 $_GET 接收连接时的参数,我们需要在这里绑定一个 onWebSocketConnect
// 的回调函数,然后在函数中就可以使用 $_GET 接收参数了
$connection->onWebSocketConnect = function ($connection, $http_header) {
// 保存这个客户端所在的房间号
$connection->room_id = $_GET['room_id'];
global $rooms;
// 把这个客户端保存到自己房间的数组中
$rooms[$_GET['room_id']][] = $connection;
};
};
// 接收消息
$worker->onMessage = function($connection, $data) {
global $worker, $rooms;
// 只循环发消息这个客户端所在的房间数组中的客户端
foreach($rooms[$connection->room_id] as $c)
{
$c->send($data);
}
};
// 当有客户端断开连接就从数组中删除
// 参数 $connection 代表断开连接的客户端
$worker->onClose = function ($connection) {
// 从房间数组中删除这个客户端
global $rooms;
// 循环这个客户端所在的房间数组,并找到这个客户端在数组中的下标 ,然后删除
foreach($rooms[$connection->room_id] as $k => $c)
{
if($c == $connection)
unset($rooms[$connection->room_id][$k]);
}
};
// 运行
Worker::runAll();
聊天室
GitHub 地址:https://github.com/fortheday001/workerman-chat.git
使用技术:websocket、workerman、vue
效果:输入用户名进入聊天室,进入之后可以开始群聊。
学习完之后的作业:
1、添加私聊功能
2、保存聊天记录
3、添加注册、登录功能(温馨提示:如果是前后端分离的方式开发,那么是不能使用 SESSION、COOKIE 的,这时需要使用令牌(JWT)实现登录功能)
服务器端
server.php
<?php
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 保存所有用户
$allUsers=[];
// 有连接时调用
function connect($connection)
{
$connection->onWebSocketConnect = function ($connection, $http_header) {
global $allUsers;
// 保存当前用户到用户列表
$allUsers[$connection->id] = ['username'=>$_GET['username']];
// 保存当前用户名到当前连接的 $connection 对象上
$connection->username = $_GET['username'];
// 给所有客户端发消息
sendToAll([
'username'=>$connection->username,
'content'=>'加入了聊天室',
'datetime'=>date('Y-m-d H:i'),
'allUsers'=>$allUsers,
]);
};
}
// 当收到数据时调用
function message($connection, $data)
{
// 转发消息给所有客户端
sendToAll([
'username'=>$connection->username,
'content'=>$data,
'datetime'=>date('Y-m-d H:i')
]);
}
// 当有客户端断开连接时调用
function close($connection)
{
global $allUsers;
// 从用户列表数组中删除当前退出的用户
unset($allUsers[$connection->id]);
// 给所有用户发消息
sendToAll([
'username'=>$connection->username,
'content'=>'离开了聊天室',
'datetime'=>date('Y-m-d H:i'),
'allUsers'=>$allUsers
]);
}
// 给所有人发消息
function sendToAll($data)
{
global $worker;
if(is_array($data))
{
$data = json_encode($data);
}
// 循环所有客户端
foreach($worker->connections as $c)
{
$c->send($data);
}
}
// 绑定端口
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数为1
$worker->count = 1;
// 设置回调函数
$worker->onConnect = 'connect';
$worker->onMessage = 'message';
$worker->onClose = 'close';
// 启动
Worker::runAll();
客户端
room.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div class="container">
<!-- 消息列表 -->
<div class="message-list box">
<h3>消息列表</h3>
<dl v-for="(v,k) in messageList" :key="k">
<dt><strong>{{v.username}}</strong> 在 <time>{{ v.datetime }}</time> 说:</dt>
<dd>{{ v.content }}</dd>
</dl>
</div>
<!-- 用户列表 -->
<div class="user-list box">
<h3>用户列表 <a @click="logout" href="#">退出</a></h3>
<ul>
<li @click="userClicked" v-for="(v,k) in userList">{{ v.username }}</li>
</ul>
</div>
</div>
<!-- 发送消息框 -->
<div class="container send-box">
<textarea v-model="message"></textarea>
<input @click="send" id="btn-send" type="button" value="发送">
</div>
<!-- 登录表单 -->
<div :class="{'login':true,'hide':layerHide}">
<div><input v-model="username" type="text" placeholder="输入用户名"></div>
<div><input @click="dologin" type="button" value="登录"></div>
</div>
<!-- 登录时黑色背景层 -->
<div :class="{'layer':true,'hide':layerHide}"></div>
</div>
<script src="vue.min.js"></script>
<script src="room.js"></script>
</body>
</html>
style.css
html,body,div,ul,form,dl,dt,dd {
margin: 0;
padding: 0;
}
a {
text-decoration: none;
}
.container {
width: 800px;
margin: 0 auto;
margin-bottom: 10px;
overflow: hidden;
}
.hide {
display: none;
}
.box {
border: 1px solid #CCC;
height: 500px;
padding: 10px;
overflow-y: scroll;
}
.box h3 {
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.message-list {
width: 70%;
float: left;
}
.message-list dl {
margin-bottom: 20px;
}
.message-list dt {
padding: 5px;
background-color: #eee;
}
.message-list dd {
padding: 10px;
}
.message-list time {
color: #999;
}
.user-list {
width: 20%;
float: right;
}
.user-list li {
cursor: pointer;
padding: 5px;
}
.user-list li:hover {
background-color: #eee;
}
.send-box {
height: 80px;
}
.send-box textarea {
height: 92%;
width: 85%;
}
#btn-send {
height: 100%;
width: 13%;
border: 1px solid #ccc;
float: right;
cursor: pointer;
font-size: 20px;
}
.layer {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: #000;
z-index: 8888;
opacity: .5;
}
.login {
position: absolute;
width: 200px;
height: 100px;
background-color: #fff;
padding: 20px;
border: 1px solid #999;
left: 150px;
top: 100px;
z-index: 9999;
}
room.js
new Vue({
el: "#app",
data: {
messageList: [], // 消息列表
userList: [], // 在线用户列表
message: '', // 消息框中的内容
ws: null, // websocket 对象
layerHide: false, // 是否隐藏登录框
host: '127.0.0.1:8686', // 服务器地址
username: localStorage.getItem('username') // 当前用户名
},
methods: {
// 退出按钮
logout: function() {
localStorage.removeItem('username')
this.username = undefined
this.layerHide = false
this.ws.close()
},
// 登录按钮
dologin: function() {
if(this.username != '')
{
localStorage.setItem('username', this.username)
this.layerHide = true
this.ws_conn()
}
},
// 发消息按钮
send: function() {
if(this.message == '')
return
this.ws.send(this.message)
this.message = ''
},
// 聊天列表滚动到底部
scrollToBottom: function() {
let d = document.querySelector('.message-list')
d.scrollTop = d.scrollHeight
},
// 收到消息时调用
ws_message: function(e){
let data = JSON.parse(e.data)
this.messageList.push(data)
setTimeout(()=>{
this.scrollToBottom()
}, 100)
if(data.allUsers)
{
this.userList = data.allUsers
}
},
// 连接服务器
ws_conn: function() {
this.ws = new WebSocket('ws://'+this.host+'?username='+this.username)
this.ws.onopen = this.ws_open
this.ws.onmessage = this.ws_message
this.ws.onclose = this.ws_close
}
},
// 页面初始化
created: function(){
if(localStorage.getItem('username'))
{
this.layerHide = true
this.ws_conn()
}
}
})
坦克大战
GitHub 地址:https://github.com/fortheday001/workerman-tankwar.git
服务器端
server.php
<?php
use Workerman\Worker;
require_once '../Workerman-master/Autoloader.php';
// 保存所有坦克
$tanks=[];
// 有连接时调用
function connect($connection)
{
$connection->onWebSocketConnect = function ($connection, $http_header) {
global $tanks;
// 把服务器上保存的所有坦克的信息发送给该客户端
$connection->send(json_encode([
'type'=>'all',
'tanks'=>$tanks
]));
// 在服务器上保存当前坦克的信息
$tanks[$connection->id] = [$_GET['left'],$_GET['top']];
// 给所有其它客户端发送添加当前坦克的消息
sendToAll([
'type'=>'add',
'id'=>$connection->id,
'left'=>$_GET['left'],
'top'=>$_GET['top']
], $connection->id);
};
}
// 当收到移动数据时调用
function message($connection, $data)
{
global $tanks;
// 更新服务器上保存的当前坦克的位置信息
$tanks[$connection->id] = explode(',', $data);
// 把坦克移动之后的位置发送给所有其它客户端
sendToAll([
'type'=>'move',
'id'=>$connection->id,
'left'=>$tanks[$connection->id][0],
'top'=>$tanks[$connection->id][1],
'transform'=>$tanks[$connection->id][2],
]);
}
// 当有客户端断开连接时调用
function close($connection)
{
global $tanks;
// 从服务器中删除当前客户端的信息
unset($tanks[$connection->id]);
// 通知所有客户端删除该坦克
sendToAll([
'type'=>'remove',
'id'=>$connection->id,
]);
}
// 功能函数:给所有客户端发消息,可以通过第二个参数排除某个客户端
function sendToAll($data, $except='')
{
global $worker;
// 如果是数组就转成 JSON 字符串
if(is_array($data))
$data = json_encode($data);
// 循环所有客户端
foreach($worker->connections as $c)
{
// 跳过要排除的客户端(不给这个客户端发消息)
if($except && $except==$c->id)
continue;
// 发消息
$c->send($data);
}
}
// 绑定端口
$worker = new Worker('websocket://0.0.0.0:8686');
// 设置进程数为1
$worker->count = 1;
// 设置回调函数
$worker->onConnect = 'connect';
$worker->onMessage = 'message';
$worker->onClose = 'close';
// 启动
Worker::runAll();
客户端
tank.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>坦克大战</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="tank.js"></script>
</body>
</html>
style.css
html,body,div,ul,form,dl,dt,dd {
margin: 0;
padding: 0;
}
a {
text-decoration: none;
}
.tank {
position: absolute;
width: 40px;
height: 30px;
background-color: yellow;
top: 0;
left: 0;
border: 1px solid #000;
}
.tank:before {
position: absolute;
left: 50%;
top: 45%;
content: "";
display: block;
width: 100%;
height: 10%;
background-color: #f00;
}
.tank:after {
position: absolute;
left: 40%;
top: 35%;
content: "";
display: block;
width: 30%;
height: 30%;
border-radius: 50%;
background-color: #f00;
}
tank.js
var my = null // 我的坦克
var allTanks = [] // 所有坦克
var ws = null // WebSocket 对象
const host = '127.0.0.1:8686' // 服务器地址
const myColor = '#4baf32' // 我的坦克颜色
const speed = 8 // 移动速度
const maxWidth = 1100 // 随机生成坦克位置时最大 left
const maxHeight = 800 // 随机生成坦克位置时最大 top
// 随机生成我的坦克所在的位置
myPos = {
left: randomBetween(0, maxWidth),
top: randomBetween(0, maxHeight)
}
// 绑定 上、下、左、右 四个键盘子的点击事件
window.onkeydown = function(e) {
switch(e.keyCode)
{
case 38:
move('up')
break
case 40:
move('down')
break
case 37:
move('left')
break
case 39:
move('right')
break
}
}
// 移动我的坦克
function move(direction) {
if(direction == 'up')
{
// 旋转
my.style.transform = 'rotate(-90deg)'
// 移动
myPos.top -= speed
my.style.top = myPos.top + 'px'
}
else if(direction == 'down')
{
my.style.transform = 'rotate(90deg)'
myPos.top += speed
my.style.top = myPos.top + 'px'
}
else if(direction == 'left')
{
my.style.transform = 'rotate(180deg)'
myPos.left -= speed
my.style.left = myPos.left + 'px'
}
else if(direction == 'right')
{
my.style.transform = 'rotate(0deg)'
myPos.left += speed
my.style.left = myPos.left + 'px'
}
ws.send(myPos.left+','+myPos.top+','+my.style.transform)
}
// 向页面中添加一个坦克
function addTank(left,top,transform) {
transform = transform || 'rotate(0deg)'
let tank = document.createElement('div')
tank.className = 'tank'
tank.style.left = left + 'px'
tank.style.top = top + 'px'
tank.style.transform = transform
document.body.append(tank)
allTanks.push(tank)
return tank
}
// 生成一个随机数
function randomBetween(Min,Max){
var Range = Max - Min;
var Rand = Math.random();
var num = Min + Math.round(Rand * Range); //四舍五入
return num;
}
// 生成我的坦克
my = addTank(myPos.left, myPos.top)
my.style.backgroundColor = myColor
// 连接服务器
ws = new WebSocket('ws://'+host+'?left='+myPos.left+'&top='+myPos.top)
// 接收并处理服务器的消息
ws.onmessage = function(e) {
let data = JSON.parse(e.data)
switch(data.type)
{
// 当消息类型为添加时,就向页面添加一个新坦克
case 'add':
allTanks[data.id] = addTank(data.left, data.top)
break
// 当消息类型为移动时,就移动坦克(通过 id 区分坦克)
case 'move':
if(allTanks[data.id]!=undefined)
{
allTanks[data.id].style.left = data.left + 'px'
allTanks[data.id].style.top = data.top + 'px'
allTanks[data.id].style.transform = data.transform
}
break
// 当收到删除的消息时,就删除这个坦克(通过 id 区分坦克)
case 'remove':
if(allTanks[data.id]!=undefined)
{
document.body.removeChild(allTanks[data.id])
allTanks.splice(data.id,1)
}
break
// 当收到所有坦克的消息时,就在页面中添加所有的坦克
case 'all':
for(let i in data.tanks)
{
allTanks[i] = addTank(data.tanks[i][0], data.tanks[i][1], data.tanks[2])
}
break
}
}