Websocket trên Laravel

Giới thiệu chung

Websocket là một khái niệm khá quen thuộc với hầu hết các developer. Nó là công nghệ hỗ trợ giao tiếp giữa client và server bằng cách sử dụng một TCP socket để tạo một kết nối hiệu quả. Mặc dù được thiết kế chuyên sử dụng cho các ứng dụng web, lập trình viên vẫn có thể đưa chúng vào bất kì loại ứng dụng nào khác.

Ưu điểm

WebSockets cung cấp khả năng giao tiếp hai chiều mạnh mẽ, có độ trễ thấp và dễ xử lý lỗi. Không cần phải có nhiều kết nối như phương pháp Comet long-polling và cũng không có những nhược điểm như Comet streaming. API cũng rất dễ sử dụng trực tiếp mà không cần bất kỳ các tầng bổ sung nào, so với Comet, thường đòi hỏi một thư viện tốt để xử lý kết nối lại, thời gian chờ timeout, các Ajax request (yêu cầu Ajax), các tin báo nhận và các dạng truyền tải tùy chọn khác nhau (Ajax long-polling và jsonp polling).

Nhược điểm

Những nhược điểm của WebSockets gồm có:

Nó là một đặc tả mới của HTML5, nên nó vẫn chưa được tất cả các trình duyệt hỗ trợ.

Không có phạm vi yêu cầu nào. Do WebSocket là một TCP socket chứ không phải là HTTP request, nên không dễ sử dụng các dịch vụ có phạm vi-yêu cầu, như SessionInViewFilter của Hibernate. Hibernate là một framework kinh điển cung cấp một bộ lọc xung quanh một HTTP request.

Khi bắt đầu một request, nó sẽ thiết lập một contest (chứa các transaction và liên kết JDBC) được ràng buộc với luồng request. Khi request đó kết thúc, bộ lọc hủy bỏ contest này.

Vậy là tôi đã giới thiệu tổng quan cho các bạn về websocket Những ưu điểm và nhược điểm khi sử dụng websocket.

Bây giờ tôi sẽ hướng dẫn các bạn sử dụng websocket trong Laravel.

Cách install Laravel các bạn có thể tham khảo ở bài viết này: https://viblo.asia/phamkykhoi/posts/ZK1ov1VxR5b9

Trong thư mục root của project các bạn tạo ra một file server.php như sau

<?php
    $host = 'localhost'; //host
    $port = '80'; //port
    $null = null; //null var

    //Create TCP/IP sream socket
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    //reuseable port
    socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

    //bind socket to specified host
    socket_bind($socket, $host, $port);

    //listen to port
    socket_listen($socket);

    //create & add listning socket to the list
    $clients = [$socket];

    //start endless loop, so that our script doesn't stop
    while (true) {
        //manage multipal connections
        $changed = $clients;
        //returns the socket resources in $changed array
        socket_select($changed, $null, $null, 0, 10);

        //check for new socket
        if (in_array($socket, $changed)) {
            $socket_new = socket_accept($socket); //accpet new socket
            $clients[] = $socket_new; //add socket to client array

            $header = socket_read($socket_new, 1024); //read data sent by the socket
            perform_handshaking($header, $socket_new, $host, $port); //perform websocket handshake

            socket_getpeername($socket_new, $ip); //get ip address of connected socket
            $response = mask(json_encode(['type' => 'system', 'message' => $ip . ' connected'])); //prepare json data
            send_message($response); //notify all users about new connection

            //make room for new socket
            $found_socket = array_search($socket, $changed);
            unset($changed[$found_socket]);
        }

        //loop through all connected sockets
        foreach ($changed as $changed_socket) {

            //check for any incomming data
            while (socket_recv($changed_socket, $buf, 1024, 0) >= 1) {
                $received_text = unmask($buf); //unmask data
                $tst_msg = json_decode($received_text); //json decode
                $user_name = $tst_msg->name; //sender name
                $user_message = $tst_msg->message; //message text
                $user_color = $tst_msg->color; //color

                //prepare data to be sent to client
                $response_text = mask(json_encode(['type' => 'usermsg', 'name' => $user_name, 'message' => $user_message, 'color' => $user_color]));
                send_message($response_text); //send data
                break 2; //exist this loop
            }

            $buf = @socket_read($changed_socket, 1024, PHP_NORMAL_READ);
            if ($buf === false) { // check disconnected client
                // remove client for $clients array
                $found_socket = array_search($changed_socket, $clients);
                socket_getpeername($changed_socket, $ip);
                unset($clients[$found_socket]);

                //notify all users about disconnected connection
                $response = mask(json_encode(['type' => 'system', 'message' => $ip . ' disconnected']));
                send_message($response);
            }
        }
    }
    // close the listening socket
    socket_close($sock);

    function send_message($msg)
    {
        global $clients;
        foreach ($clients as $changed_socket) {
            @socket_write($changed_socket, $msg, strlen($msg));
        }

        return true;
    }

    //Unmask incoming framed message
    function unmask($text)
    {
        $length = ord($text[1]) & 127;
        if ($length == 126) {
            $masks = substr($text, 4, 4);
            $data = substr($text, 8);
        } elseif ($length == 127) {
            $masks = substr($text, 10, 4);
            $data = substr($text, 14);
        } else {
            $masks = substr($text, 2, 4);
            $data = substr($text, 6);
        }
        $text = "";
        for ($i = 0; $i < strlen($data); ++$i) {
            $text .= $data[$i] ^ $masks[$i % 4];
        }

        return $text;
    }

    //Encode message for transfer to client.
    function mask($text)
    {
        $b1 = 0x80 | (0x1 & 0x0f);
        $length = strlen($text);

        if ($length <= 125)
            $header = pack('CC', $b1, $length);
        elseif ($length > 125 && $length < 65536)
            $header = pack('CCn', $b1, 126, $length);
        elseif ($length >= 65536)
            $header = pack('CCNN', $b1, 127, $length);

        return $header . $text;
    }

    //handshake new client.
    function perform_handshaking($receved_header, $client_conn, $host, $port)
    {
        $headers = [];
        $lines = preg_split("/\r\n/", $receved_header);
        foreach ($lines as $line) {
            $line = chop($line);
            if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
                $headers[$matches[1]] = $matches[2];
            }
        }

        $secKey = $headers['Sec-WebSocket-Key'];
        $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        //hand shaking header
        $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
            "Upgrade: websocket\r\n" .
            "Connection: Upgrade\r\n" .
            "WebSocket-Origin: $host\r\n" .
            "WebSocket-Location: ws://$host:$port/server.php\r\n" .
            "Sec-WebSocket-Accept:$secAccept\r\n\r\n";
        socket_write($client_conn, $upgrade, strlen($upgrade));
    }

Tiếp theo các bạn tạo một form chat đơn giản như sau

resources\views\chat.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta charset='UTF-8'/>
    <style type="text/css">
        .chat_wrapper {
            width: 500px;
            margin-right: auto;
            margin-left: auto;
            background: #CCCCCC;
            border: 1px solid #999999;
            padding: 10px;
            font: 12px 'lucida grande', tahoma, verdana, arial, sans-serif;
        }

        .chat_wrapper .message_box {
            background: #FFFFFF;
            height: 150px;
            overflow: auto;
            padding: 10px;
            border: 1px solid #999999;
        }

        .chat_wrapper .panel input {
            padding: 2px 2px 2px 5px;
        }
        .system_msg { color: #BDBDBD; font-style: italic; }
        .user_name { font-weight: bold; }
        .user_message { color: #88B6E0; }
    </style>
</head>
<body>
    <div class="chat_wrapper">
        <div class="message_box" id="message_box"></div>
        <div class="panel">
            <input type="text" name="name" id="name" placeholder="Your Name" maxlength="10" style="width:20%"/>
            <input type="text" name="message" id="message" placeholder="Message" maxlength="80" style="width:60%"/>
            <button id="send-btn">Send</button>
        </div>
    </div>
</body>
</html>

Tiếp theo các bạn tạo một file

public\chat.js

$(document).ready(function () {
    //create a new object WebSocket
    // Ở đây domain VirtualHost tôi để là laravel.dev
    var wsUri = "ws://laravel.dev/server.php";
    websocket = new WebSocket(wsUri);

    websocket.onopen = function (ev) { // connection is open
        $('#message_box').append("<div class=\"system_msg\">Connected!</div>"); //notify user
    }

    $('#send-btn').click(function () { //use clicks message send button
        var mymessage = $('#message').val(); //get message text
        var myname = $('#name').val(); //get user name

        if (myname == "") { //empty name?
            alert("Enter your Name please!");
            return;
        }
        if (mymessage == "") { //emtpy message?
            alert("Enter Some message Please!");
            return;
        }

        //prepare json data
        var msg = {
            message: mymessage,
            name: myname,
            color: '<?php echo $colours[$user_colour]; ?>'
        };

        //convert and send data to server
        websocket.send(JSON.stringify(msg));
    });

    // Return message from server
    websocket.onmessage = function (ev) {
        var msg = JSON.parse(ev.data); //PHP sends Json data
        var type = msg.type; //message type
        var umsg = msg.message; //message text
        var uname = msg.name; //user name
        var ucolor = msg.color; //color

        if (type == 'usermsg' && umsg != null) {
            $('#message_box').append("<div><span class=\"user_name\" style=\"color:#" + ucolor + "\">" + uname + "</span> : <span class=\"user_message\">" + umsg + "</span></div>");
        }
        if (type == 'system' && umsg != null) {
            $('#message_box').append("<div class=\"system_msg\">" + umsg + "</div>");
        }

        $('#message').val(''); //reset text
    };

    websocket.onerror = function (ev) {
        $('#message_box').append("<div class=\"system_error\">Error Occurred - " + ev.data + "</div>");
    };
    websocket.onclose = function (ev) {
        $('#message_box').append("<div class=\"system_msg\">Connection Closed</div>");
    };
});

Ở file resources\views\chat.blade.php các bạn nhúng thêm file js vào tạo vào.

<script src='chat.js'></script>

Tiếp theo các bạn tạo một routes để chạy views chat của mình

Route::get('chat', function () {
    return view('chat');
});

Bây giờ các bạn chạy web lên với domain là http://laravel.dev/chat Sau đó các bạn mở command line lên. Truy xuất vào thư mục project của mình và chạy lệnh.

php server.php

Lúc này các bạn có thể sử dụng được ứng dụng chat của mình với websocket trên Laravel.

Ở bài này tôi chia sẻ cho các bạn cách để có thể chạy được websocket trên Laravel. Ở những bài sau tôi sẽ chia sẽ kỹ hơn các vấn đề trong websocket.

Chúc các bạn học tốt !


All Rights Reserved