laravel integrates Workerman to build websocket service, front-end call, server startup

This article has relatively complete integration code and front-end test code, as well as server startup considerations.

1. Backend (laravel5.5)

1. Composer installs Workerman

composer require workerman/workerman

2. Generate an execution command. After execution, a Workerman.php file will be generated in the App\Console\Command folder.

php artisan make:command Workerman

3. Open the Workerman.php file and modify the code as follows:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Tymon\JWTAuth\JWTAuth;
use Workerman\Timer;
use Workerman\Worker;
//Heartbeat interval 55 seconds
define('HEARTBEAT_TIME', 3600);
class Workerman extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'Workerman {action} {--daemonize}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        global $argv; //Define global variables
        $arg = $this->argument('action');
        $argv[1] = $arg;
        $argv[2] = $this->option('daemonize') ? '-d' : '';//This parameter is started in daemon (daemon process) mode

        global $text_worker;
        //Create a Worker to listen to port 9991 and communicate using the websocket protocol
        $text_worker = new Worker("websocket://0.0.0.0:9991");
        $text_worker->uidConnections = array();//Online user connection object
        $text_worker->uidInfo = array();//User information of online users
        //Start 4 processes to provide external services
        $text_worker->count = 1;
        //This method is triggered when workererman is started
        $text_worker->onWorkerStart =function(){
            //Listen to an internal port to receive messages from the server and forward them to the browser
            $inner_text_worker = new Worker('text://127.0.0.1:5678');
            $inner_text_worker->onMessage = function($connection_admin, $data)
            {
                global $text_worker;
                //$data array format, which contains uid, means pushing data to the page with that uid
                $buffer = json_decode($data, true);
                //var_dump($buffer);
                $to_uid = $buffer['to_uid'];
                //var_dump($to_uid);

                //Push data to the page of uid through workererman
                if(isset($text_worker->uidConnections[$to_uid])){
                    $connection = $text_worker->uidConnections[$to_uid];
                    $ret = $connection->send($data);
                } else {
                    var_dump($to_uid . ': not define');
                    $ret = false;
                }
                //Return push results
                $connection_admin->send($ret ? 'ok' : 'fail');
            };

            $inner_text_worker->listen();

            // After the process starts, set a timer that runs every 10 seconds
            Timer::add(10, function(){
                global $text_worker;
                $time_now = time();
                foreach($text_worker->connections as $connection) {
                    // It is possible that the connection has not received a message, so lastMessageTime is set to the current time.
                    if (empty($connection->lastMessageTime)) {
                        $connection->lastMessageTime = $time_now;
                        continue;
                    }
                    // If the last communication time interval is greater than the heartbeat interval, the client is considered offline and the connection is closed.
                    if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                        var_dump("delete:" . $connection->uid);
                        unset($text_worker->uidConnections["{$connection->uid}"]);
                        $connection->close();
                    }
                }
            });
        };
        //This function is triggered when the browser connects
        $text_worker->onConnect = function($connection) {
        };
        //Triggered when sending information to the user
        //$connection information about the currently connected person $data data sent
        $text_worker->onMessage = function($connection,$data){
            // Temporarily set a lastMessageTime attribute for the connection to record the time when the last message was received.
            $connection->lastMessageTime = time();
            //Other business logic...
            $data = json_decode($data, true);
            if($data['type']=='login') {
                $this->create_uid($connection,$data);
            }
            if($data['type']=='send_message'){
                $this->send_message($connection,$data);
            }
        };
        //Triggered when the browser disconnects the link
        $text_worker->onClose = function($connection){};
        Worker::runAll();
    }

    //Create uid method
    public function create_uid($connection,$data){
        global $text_worker;
        $connection->uid = $data['uid'];
        //Save the user's uid
        $text_worker->uidConnections["{$connection->uid}"] = $connection;
        //Return successful creation information to your browser
        $connection->send(json_encode([
            "uid"=>$connection->uid,
            "msgType"=>'text',
            "content"=>'The chat is created successfully, you can develop and send messages'
        ]));
    }

    public function send_message($connection,$data) {
        global $text_worker;
        if(isset($data['to_uid'])){
            if(isset($text_worker->uidConnections["{$data['to_uid']}"])){
                $to_connection=$text_worker->uidConnections["{$data['to_uid']}"];
                $to_connection->send(json_encode([
                    "uid"=>$data['uid'],
                    "msgType"=>$data['msgType'],
                    "content"=>$data['message']
                ]));
            }
        }
    }
}

4. Modify the Kernel.php file under App\Console as follows:

protected $commands = [
        Commands\Workerman::class,
];

5. Start the Workerman service. The startup command is as follows:

php artisan Workerman start

If the above picture appears, it means the startup is successful.

2. How to connect the front-end to the Workerman service (websocket) service

1. Create two new HTML pages for testing. Of course, you can also directly open F12 in the browser and test in the console. I have a written page here. You can use it by simply changing the ws address. It supports sending text and pictures.

37websoket.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
  />
  <script src="//i2.wp.com/cdn.jsdelivr.net/npm/vue@2"></script> <!-- Introduce vue.js -->
</head>
<body>
  <div id="app">
    <div style="padding: 10px;height: 76vh;overflow-y: scroll;" ref="scrollDiv">
      <div v-for="(item,index) in content" :key="index">
        <div class="left-box" v-if="item.uid == 38">
          <div>
            <img src="./nv.png" style="width:35px;flex-shrink: 0;">
          </div>
          <div class="msg-box-l" v-if="item.msgType=='text'">
            <div class="msg-text-l">{<!-- -->{item.content}}</div>
          </div>
          <div class="msg-box-l" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
            <img :src="item.content" style="width: 100%;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
          </div>
        </div>
        <div class="right-box" v-else>
          <div class="msg-box-r" v-if="item.msgType=='text'">
            <div class="msg-text-r">{<!-- -->{item.content}}</div>
          </div>
          <div class="msg-box-r" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
            <img :src="item.content" style="width: 100%;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
          </div>
          <div>
            <img src="./nan.png" style="width:35px;flex-shrink: 0;">
          </div>
        </div>
      </div>
    </div>
    
  <div class="ta">
        <textarea type="text" name="" v-model="msg" class="ta1"></textarea>
        <div class="r-b">
          <input type="file" id="file" @change="sendPic" class="input-file" />
          <div @click="send" class="send-btn">Send a message</div>
          <label for="file" class="send-btn">Send pictures</label>
        </div>
    </div>
  </div>
  <script>
    newVue({
      el: '#app',
      data: {
          content:[],
          msg: "",
          path:"ws://127.0.0.1:9991//ws",
          socket:""
      },
      mounted () {
        // initialization
        this.init()
      },
      methods:{
        scrollToBottom() {
          this.$nextTick(() => {
            let scrollElem = this.$refs.scrollDiv;
            scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
          });
        },
        sendPic(file) {
          console.log(file)
          const reader = new FileReader();
          reader.readAsDataURL(file.target.files[0]);
          reader.onload = () => {
            // const src = reader.result;
            this.content.push({
              uid: 37,
              msgType: 'img',
              content: reader.result
            })
            this.socket.send(`{"type":"send_message","to_uid":38,"uid":37,"msgType":"img", "message":"${reader.result}"}` )
            this.scrollToBottom()
            // The reader.result here is the base64 of the file. If it is a picture, it can be displayed directly in src.
          };
        },
      init: function () {
            if(typeof(WebSocket) === "undefined"){
                alert("Your browser does not support socket")
            }else{
                // Instantiate socket
                this.socket = new WebSocket(this.path)
                //Listen to socket connection
                this.socket.onopen = this.open
                // Listen for socket error messages
                this.socket.onerror = this.error
                //Listen to socket messages
                this.socket.onmessage = this.getMessage
            }
        },
        open: function () {
          this.socket.send('{"uid":37,"type":"login"}');
            console.log("socket connection successful")
        },
        error: function () {
            console.log("Connection error")
        },
        getMessage: function (msg) {
          this.content.push({
            uid: JSON.parse(msg.data).uid,
            msgType: JSON.parse(msg.data).msgType,
            content: JSON.parse(msg.data).content
          })
          this.scrollToBottom()
            console.log(msg.data)
        },
        //Send a message to the connected server
        send: function () {
          this.content.push({
              uid: 37,
              msgType: 'text',
              content: this.msg
            })
            this.socket.send(`{"type":"send_message","to_uid":38,"uid":37,"msgType":"text","message":"${this.msg.replace(/ \
/g,"\\
").replace(/\r/g,"\r")}"}`)
            this.scrollToBottom()
            this.msg = ""
        },
        close: function () {
            console.log("socket has been closed")
        }
      },
      // destroyed () {
      // // Destroy the listener
      // this.socket.onclose = this.close
      // }
    });
  </script>
</body>
<style type="text/css">
  html,body{margin: 0;padding:0}
  #app{height: 100vh;background: #F5F5F5}
  .left-box {display: flex;flex-shrink: 0;margin-bottom: 10px;margin-top: 10px;}
  .right-box {display: flex;justify-content: flex-end;width: 100%;margin-bottom: 10px;margin-top: 10px;}
  .msg-box-l{background: #fff;max-width: 80%;padding: 8px;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius : 10px;margin-left: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
  .msg-box-r{background: #95EC69;max-width: 80%;padding: 8px;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius : 10px;margin-right: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
  .msg-text-l{color: #000;word-break: break-all;}
  .msg-text-r{color: #000;word-break: break-all;}
.ta{resize: none;width: 100%;height: 100px;position: fixed;bottom: 20px;}
.ta1{resize: none;width: 70%;margin-left:5%;height: 100px;position: fixed;bottom: 20px;}
.r-b{display: flex;align-items: flex-end;flex-direction: column;}
.send-btn{background: #4287FD;color: #fff;padding: 5px 10px;margin: 10px;}
.input-file {
    width: 0.1px;
    height: 0.1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;

</style>
</html>

38websoket.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
  />
  <script src="//i2.wp.com/cdn.jsdelivr.net/npm/vue@2"></script> <!-- Introduce vue.js -->
</head>
<body>
  <div id="app">
    <div style="padding: 10px;height: 76vh;overflow-y: scroll;" ref="scrollDiv">
      <div v-for="(item,index) in content" :key="index">
        <div class="left-box" v-if="item.uid == 37">
          <div>
            <img src="./nv.png" style="width:35px;flex-shrink: 0;">
          </div>
          <div class="msg-box-l" v-if="item.msgType=='text'">
            <div class="msg-text-l">{<!-- -->{item.content}}</div>
          </div>
          <div class="msg-box-l" v-if="item.msgType=='img'" style="max-width:40%;">
            <img :src="item.content" style="width: 100%;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
          </div>
        </div>
        <div class="right-box" v-else>
          <div class="msg-box-r" v-if="item.msgType=='text'">
            <div class="msg-text-r">{<!-- -->{item.content}}</div>
          </div>
          <div class="msg-box-r" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
            <img :src="item.content" style="width: 100%;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
          </div>
          <div>
            <img src="./nan.png" style="width:35px;flex-shrink: 0;">
          </div>
        </div>
      </div>
    </div>
    
    <div class="ta">
        <textarea type="text" name="" v-model="msg" class="ta1"></textarea>
        <div class="r-b">
          <input type="file" id="file" @change="sendPic" class="input-file" />
          <div @click="send" class="send-btn">Send a message</div>
          <label for="file" class="send-btn">Send pictures</label>
        </div>
    </div>
  </div>
  <script>
    newVue({
      el: '#app',
      data: {
          content:[],
          msg: "",
          path:"ws://127.0.0.1:9991//ws",
          socket:""
      },
      mounted () {
        // initialization
        this.init()
      },
      methods:{
        scrollToBottom() {
          this.$nextTick(() => {
            let scrollElem = this.$refs.scrollDiv;
            scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
          });
        },
        sendPic(file) {
          console.log(file)
          const reader = new FileReader();
          reader.readAsDataURL(file.target.files[0]);
          reader.onload = () => {
            // const src = reader.result;
            this.content.push({
              uid: 38,
              msgType: 'img',
              content: reader.result
            })
            this.socket.send(`{"type":"send_message","to_uid":37,"uid":38,"msgType":"img", "message":"${reader.result}"}` )
            this.scrollToBottom()
            // The reader.result here is the base64 of the file. If it is a picture, it can be displayed directly in src.
          };
        },
        init: function () {
            if(typeof(WebSocket) === "undefined"){
                alert("Your browser does not support socket")
            }else{
                // Instantiate socket
                this.socket = new WebSocket(this.path)
                //Listen to socket connection
                this.socket.onopen = this.open
                // Listen for socket error messages
                this.socket.onerror = this.error
                //Listen to socket messages
                this.socket.onmessage = this.getMessage
            }
        },
        open: function () {
          this.socket.send('{"uid":38,"type":"login"}');
            console.log("socket connection successful")
        },
        error: function () {
            console.log("Connection error")
        },
        getMessage: function (msg) {
          this.content.push({
            uid: JSON.parse(msg.data).uid,
            msgType: JSON.parse(msg.data).msgType,
            content: JSON.parse(msg.data).content
          })
          this.scrollToBottom()
            console.log(msg.data)
        },
        //Send a message to the connected server
        send: function () {
          this.content.push({
              uid: 38,
              msgType: 'text',
              content: this.msg
            })
            this.socket.send(`{"type":"send_message","to_uid":37,"uid":38,"msgType":"text","message":"${this.msg.replace(/ \
/g,"\\
").replace(/\r/g,"\r")}"}`)
            this.scrollToBottom()
            this.msg = ""
        },
        close: function () {
            console.log("socket has been closed")
        }
      },
      // destroyed () {
      // // Destroy the listener
      // this.socket.onclose = this.close
      // }
    });
  </script>
</body>
<style type="text/css">
  html,body{margin: 0;padding:0}
  #app{height: 100vh;background: #F5F5F5}
  .left-box {display: flex;flex-shrink: 0;margin-bottom: 10px;margin-top: 10px;}
  .right-box {display: flex;justify-content: flex-end;width: 100%;margin-bottom: 10px;margin-top: 10px;}
  .msg-box-l{background: #fff;max-width: 80%;padding: 8px;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius : 10px;margin-left: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
  .msg-box-r{background: #95EC69;max-width: 80%;padding: 8px;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius : 10px;margin-right: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
  .msg-text-l{color: #000;word-break: break-all;}
  .msg-text-r{color: #000;word-break: break-all;}
.ta{resize: none;width: 100%;height: 100px;position: fixed;bottom: 20px;}
.ta1{resize: none;width: 70%;margin-left:5%;height: 100px;position: fixed;bottom: 20px;}
.r-b{display: flex;align-items: flex-end;flex-direction: column;}
.send-btn{background: #4287FD;color: #fff;padding: 5px 10px;margin: 10px;}
.input-file {
    width: 0.1px;
    height: 0.1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;

</style>
</html>

Save them as two html files respectively. Open both in the browser. If “The chat is created successfully, you can develop and send messages” appears on the page, it means that the connection is successful and you can send messages normally.

Debugging is now complete!

3. Run the Workerman service on the server

After we complete local debugging, we finally need to upload it to the server to run, such as Centos. The service also needs composer to install Workerman. The method is the same as at the beginning. I will directly talk about how to start it.

There is a certain difference between starting Workerman on the server and locally, because usually we hope that Websocket can always be started on the server. Therefore, the above-mentioned startup command needs to be slightly changed, that is, add:- after the command:- -daemonize (daemon process startup)

php artisan Workerman start --daemonize (turn off the terminal, the service will not stop)

php artisan Workerman stop --daemonize (service stop)

php artisan Workerman start (turn off the terminal, the service will stop)

php artisan Workerman stop (service stop)

At this point, laravel integrating Workerman to build websocket service has been completed.

Reference article for this article: Combination of laravel and workerman_laravel workerman-CSDN blog