workerman 简单入门

WebSocket

什么是 WebSocket

WebSocket协议是基于 TCP 的一种新的 网络协议

它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

WebSocket 和 HTTP 的区别

HTTP协议的特点:

​ 1、服务器只能响应客户端的请求,不能主动向客户端推送数据

​ 2、客户端的每次请求都需要连接、断开,即每次请求都是一个全新的请求

WebSocket的特点:

​ 1、客户端与服务器端在连接时可以互相推据数据

​ 2、客户端连接到服务器之后,会一直保持连接的状态,直到有一端主动断开连接

应用范围

WebSocket 一般用来开发聊天室网页游戏等需要长连接并且可以双向通信的应用程序。

image-20181109162131008

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 中最核心的两个类是 WorkerTcpConnection 类。

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 为脚本的名字)

image-20181109154349862

客户端

使用 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>

前、后端图解:

image-20181109160733327

连接时传递参数

有时,我们希望在客户端连接服务器时就传递一些参数,这时可以这样做:

客户端在连接时通过 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

效果:输入用户名进入聊天室,进入之后可以开始群聊。

image-20181110112522268

学习完之后的作业:

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
    }
}

转载请注明: qqwBlog workerman 简单入门

本篇
workerman 简单入门 workerman 简单入门
WebSocket什么是 WebSocketWebSocket协议是基于 TCP 的一种新的 网络协议。 它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。 WebSocket 和 HTTP 的区
2019-01-07
下一篇
GET和POST两种基本请求方法的区别 GET和POST两种基本请求方法的区别
GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二。 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。 你可能自己写过无数个GET和POST请求,或者已经
2018-12-27