Translate

Socket-соединения в Веб-приложениях

Конечно же в JavaScript`е нету сокетов. Но зато есть такая чудесная вещь как AJAX, а также есть еще более привлекательная вещь как Comet. И с помощью этих двух технологий можно добиться от веб-страницы поразительного сходства с прикладными программами по части socket-соединений.

Хочу немного расказать о сокетах своими словами. Сокет — это что-то вроде радио-приемника, который появляется на сервере после соединения с ним клиента, для обратной связи с клиентским приложением. Как только в этот приемник попадают (записываются) данные, он сразу отправляет их своему приложению-слушателю. Приложение, услышав (прочитав) данные, выполняет какие-либо действия на их основе. Это может быть простое сообщение, которое следует напечатать в чате, или команда которую нужно выполнить итд. Если не углубляться в подробности работы TCP/IP, то примерно так все и происходит.

Ну а теперь давайте вернемся к нашим бара… сокетам. Итак, что нам нужно? Нам нужно сделать так, чтобы как только один соединенный с сервером клиент что-то отправит на сервер, это сразу же было отправлено всем другим соединенным в данный момент с сервером клиентам. Или другая ситуация. Допустим при определенных обстоятельствах послать с сервера сообщение всем соединенным с ним клиентам, и чтобы они его мгновенно (!) получили. Как этого добиться? Я раскажу ниже.

Итак, определим что нам нужно для этого:
  • Один php-файл для сервера.
  • Один html-файл для клиента.
  • Библиотека jQuery.
  • И папка sockets для хранения сокетов.

Для начала я приведу листинги клиента и сервера, а потом раскажу как воспользоваться готовым примером.

Клиент (index.html):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"«http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd»>

<html xmlns=«http://www.w3.org/1999/xhtml»>
   
    <head>
        <title>Socket AppClient</title>
        <meta http-equiv=«content-type»content=«text/html; charset=utf-8» />
        <meta name=«author» content=«Melnaron»/>
        <link rel=«stylesheet» type=«text/css»href=«css/style.css» />
        <style type=«text/css»>
           
            #log {
               border: 1px solid #999999;
               height: 250px;
               overflow: auto;
               margin: 10px 0;
               padding: 10px;
            }
           
        </style>
        <script type=«text/javascript»src=«lib/jquery.js»></script>
        <script type=«text/javascript»>
           
           $(document).ready(function() {
               
                /*
                * Настройка AJAX
                */
               $.ajaxSetup({url: 'server.php', type: 'post', dataType: 'json'});
               
                /*
                * События кнопок и поля ввода
                */
               $('#btnConnect').click(user.Connect);
               $('#btnDisconnect').click(user.Disconnect);
               $('#btnSend').click(user.Send);
               $('#input')
                   .keydown(function(e){ if (e.keyCode == 13) { user.Send();return false; } })
                   .keypress(function(e){ if (e.keyCode == 13) { return false; }})
                   .keyup(function(e){ if (e.keyCode == 13) { return false; } })
                ;
               
            });
           
        </script>
        <script type=«text/javascript»>
           
            /*
             * Лог
             */
            var log = {
               
               print: function(s) {
                  $('#log').append('<div>'+s+'</div>').get(0).scrollTop += 100;
                }
               
            };
           
            /*
             * Действияприсылаемые с сервера
             */
            var actions = {
               
               Connect: function(params) {
                   log.print('Connected.');
                   log.print('Sock: '+params.sock);
                   user.sock = params.sock;
                   user.conn = true;
                   user.Read();
                },
               
               Disconnect: function(params) {
                   log.print('Disconnected.');
                },
               
               Print: function(params) {
                   log.print(params.message);
                }
               
            };
           
            /*
             * Пользователь(клиент)
             */
            var user = {
               
               sock: null,
               
               conn: false,
               
               busy: false,
               
               read: null,
               
                /*
                * Эта функция обрабатывает приходящие с сервера действия и выполняет их.
                */
               onSuccess: function(data) {
                   if (typeof data.actions == 'object') {
                       for (var i = 0; i <data.actions.length; i++) {
                           if (typeofactions[data.actions[i].action] == 'function') {
                              actions[data.actions[i].action](data.actions[i].params);
                           }
                       }
                   }
                },
               
                /*
                * Эта функция выполняется по завершении ajax-запроса.
                */
               onComplete: function(xhr) {
                   if (xhr.status == 404) {
                       actions.Disconnect();
                   }
                   user.busy = false;
                },
               
                /*
                * Эта функция выполняется по завершении запроса-слушания.
                * При удачном завершении запроса (==200) моментальное возобновлениепрослушивания соккета.
                * При неудачном (!=200) возобновление через 5 секунд.
                */
               onCompleteRead: function(xhr) {
                   if (xhr.status == 200) {
                       user.Read();
                   } else {
                       setTimeout(user.Read, 5000);
                   }
                },
               
                /*
                * Действие.
                * Соединение с сервером.
                */
               Connect: function() {
                   if (user.conn == false && user.busy == false) {
                       log.print('Connecting...');
                       user.busy = true;
                       $.ajax({
                           data: 'action=Connect',
                           success:user.onSuccess,
                           complete:user.onComplete
                       });
                   }
                },
               
                /*
                * Действие.
                * Отсоединение от сервера.
                */
               Disconnect: function() {
                   if (user.conn && user.busy == false &&user.read) {
                       log.print('Disconnecting...');
                       user.busy = true;
                       $.ajax({
                           data:'action=Disconnect&sock='+user.sock,
                           success:user.onSuccess,
                           complete:user.onComplete
                       });
                       user.sock = null;
                       user.conn = false;
                       user.read.abort();
                   }
                },
               
                /*
                * Действие.
                * Отправка данных на сервер.
                */
               Send: function() {
                   if (user.conn) {
                       var data = $.trim($('#input').val());
                       if (!data) {
                           return;
                       }
                       $.ajax({
                           data:'action=Send&sock='+user.sock+'&data='+data,
                           success:user.onSuccess,
                           complete:user.onComplete
                       });
                       $('#input').val('');
                   } else {
                       log.print('Please connect.');
                   }
                },
               
                /*
                * Действие.
                * Прослушивание соккета.
                */
               Read: function() {
                   if (user.conn) {
                       user.read = $.ajax({
                           data:'action=Read&sock='+user.sock,
                           success:user.onSuccess,
                           complete:user.onCompleteRead
                       });
                   }
                }
               
            };
           
        </script>
    </head>
   
    <body>
        <input id=«btnConnect» type=«button»value=«Connect» />
        <input id=«btnDisconnect» type=«button»value=«Disconnect» />
        <div id=«log»></div>
        <input id=«input» type=«text» />
        <input id=«btnSend» type=«button»value=«Send» />
    </body>
   
</html>



Сервер (server.php):

<?php

class Server {
   
    /*
     * Стек действий для отправки клиенту в текущем запросе.
     */
    static private $actions;
   
    /*
     * Основная функция сервера.
     * Запускает на выполнение переданное клиентом действие.
     */
    static function Run() {
        foreach (glob('sockets/*') as $sock) {
            if (filesize($sock)> 2048) {
               unlink($sock);
            }
        }
        $action = 'action'.$_POST['action'];
        if (is_callable('self::'.$action)) {
            self::$action();
            self::Send();
        }
    }
   
    /*
     * Эта функция пишет действие в сокеты.
     * Если передан параметр $self, то исключает указанный в этом параметре сокет.
     */
    static function AddToSock($action, $params = '', $self =null) {
        foreach (glob('sockets/*') as $sock) {
            if ($self &&strpos($sock, $self) !== false) {
               continue;
            }
            $f = fopen($sock,'a+b') or die('socket not found');
            flock($f, LOCK_EX);
            fwrite($f, '{action:"'.$action.'", params: {'.$params.'}}'."\r\n");
            fclose($f);
        }
    }
   
    /*
     * Эта функция добавляет действие в стек для отправки в текущем запросе.
     */
    static function AddToSend($action, $params = '') {
        self::$actions[] = '{action:"'.$action.'", params: {'.$params.'}}';
    }
   
    /*
     * Отправка стека действий на выполнение клиенту.
     */
    static function Send() {
        if (self::$actions) {
            exit('{actions:['.implode(', ', self::$actions).']}');
        }
    }
   
    /*
     * Действие.
     * Соединение с сервером.
     * Создает сокет и отправляет его идентификатор клиенту.
     */
    static function actionConnect() {
        $sock = md5(microtime().rand(1, 1000));
        fclose(fopen('sockets/'.$sock, 'a+b'));
        self::AddToSock('Print', 'message: «Clientconnected.»', $sock);
        self::AddToSend('Connect', 'sock:"'.$sock.'"');
    }
   
    /*
     * Действие.
     * Отсоединение от сервера.
     * Удаляет сокет.
     */
    static function actionDisconnect() {
        $sock = $_POST['sock'];
        unlink('sockets/'.$sock);
        self::AddToSock('Print', 'message: «Client disconnected.»');
        self::AddToSend('Disconnect');
    }
   
    /*
     * Действие.
     * Отправляет введенные данные всем клиентам.
     */
    static function actionSend() {
        $sock = $_POST['sock'];
        $data =htmlspecialchars(trim($_POST['data']), ENT_QUOTES);
        if (strlen($data)) {
           self::AddToSock('Print', 'message: "'.$data.'"', $sock);
           self::AddToSend('Print', 'message: "'.$data.'"');
        }
    }
   
    /*
     * Действие.
     * Слушает сокет до момента когда в нем появятся данные или же до истечения таймаута.
     */
    static function actionRead() {
        $sock = $_POST['sock'];
        $time = time();
        while ((time() — $time) < 30) {
            if ($data =file_get_contents('sockets/'.$sock)) {
                $f =fopen('sockets/'.$sock, 'r+b') or die('socket not found');
               flock($f, LOCK_EX);
               ftruncate($f, 0);
               fwrite($f, '');
               fclose($f);
               $data = trim($data, "\r\n");
               foreach (explode("\r\n", $data) as $action) {
                   self::$actions[] = $action;
                }
               self::Send();
            }
            usleep(250);
        }
    }
   
}

if (@$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
   
    header('content-type: text/plain; charset=utf-8');
   
    Server::Run();
   
}

?>



Для того чтобы увидеть это чудо в действии вам следует открыть приведенную ниже ссылку в 2х или более окнах браузера (или вообще в разных браузерах).

Итак, работающий пример находится тут.

Открываем, распологаем окна так чтобы работая в одном окне видить что происходит в другом. Соединяемся с сервером.

Соединив с сервером первое окно-приложение вы увидите идентификатор своего сокета отправленный с сервера. Соединив второе окно, в первом вы тут же увидите сообщение о соединении клиента. А теперь попробуйте наблюдая за первым окном что-то отправить из второго. Я надеюсь интернет у вас не очень медленный/загруженный, и вы увидите как быстро сообщение отобразится в обоих окнах — почти мгновенно.

Итак, это простой пример. В коде конечно можно еще покопаться. Для старта я думаю хватит.

Исходники полностью работающего примера можно скачать тут.


http://habrahabr.ru/post/41223/

Комментариев нет:

Отправить комментарий

Постоянные читатели