По моему мнению, разрабатывая небольшие динамические веб-приложения вполне достаточно использование «запрос-ответ» через тривиальный Ajax. Но порой встречается задача, когда клиенту в браузер надо отправить информацию как можно быстрее (например, при изменении состояния чего-либо на сервере). Конечно, можно повесить таймер каждую секунду и по тому-же Ajax-у проверять это состояние. Но когда у вас на странице собирается несколько тысяч посетителей, эти запросы впустую ддосят веб-сервер. Наиболее подходящим решением в такой ситуации является использование веб-сокета, поднятого на необходимом порту.
Код на странице клиента
<pre id='pre'>...</pre> <script> $(function () { var socket = new WebSocket("wss://conture.by:8800"); socket.onopen = function(){ socket.send("Connect OK"); }; socket.onmessage = function(e){ $("#pre").text(e.data); }; socket.onerror = function(){ $("#pre").text("Error conection!"); }; ; socket.onclose = function(){ socket.close(); }; }); </script>
С сервером немного посложнее. Ложим его по адресу /usr/src/socket.php и даём права 0777 на запуск..
#!/usr/bin/php <?php error_reporting(E_ALL); set_time_limit(0); $logfile = __DIR__ . "/socket.log"; // если лог не нужен просто закомментировать $port = (@$argv[1]>1023 and $argv[1]<65536) ? $argv[1] : 8888; exec("ifconfig | egrep -o '([[:digit:]]{1,3}\.){3}[[:digit:]]{1,3}'", $o); $ssl = (@$argv[2]) ? true : false; $serv = ($ssl ? "ssl" : "tcp") . "://{$o[0]}:{$port}"; if($ssl){ $le = "/etc/letsencrypt/archive/{$argv[2]}"; if(is_dir($le)){ $k = @end(scandir($le)); // последний файл из директории вида privkey2.pem preg_match("/([0-9]{1,})\.pem$/", $k, $m); $fcs = "{$le}/sock{$m[1]}.pem"; // номер последней версии сертификатов if(!file_exists($fcs)){ exec("cat {$le}/cert{$m[1]}.pem {$le}/privkey{$m[1]}.pem > {$fcs}"); } slog($fcs, "CERTIFICATE"); }else{ slog($le, "NOT FOUND"); exit; } $context = stream_context_create(); stream_context_set_option($context, 'ssl', 'local_cert', $fcs); stream_context_set_option($context, 'ssl', 'passphrase', ''); stream_context_set_option($context, 'ssl', 'allow_self_signed', true); stream_context_set_option($context, 'ssl', 'verify_peer', false); $socket = stream_socket_server($serv, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); }else{ $socket = stream_socket_server($serv, $errno, $errstr); } if(!$socket) die("{$errstr} ({$errno})\n"); slog($serv, "START"); $t = 0; $connects = array(); while(1){ $read = $connects; $read[] = $socket; $write = null; $except = null; stream_select($read, $write, $except, null); if (in_array($socket, $read)) { if (($connect = stream_socket_accept($socket, -1)) && $info = handshake($connect)) { $connects[] = $connect; slog("handshake with {$info['ip']}", $connect); } unset($read[array_search($socket, $read)]); } if($t != time()){ // произошло событие!!! отправляем всем клиентам сообщение.. foreach($connects as $connect) { fwrite($connect, encode(date("H:i:s") . " {$connect}")); } $t = time(); } } function slog($s, $t=false){ global $logfile; $s = date("d.m.Y H:i:s") . " [" . ($t ? $t : "SYSTEM") . "] {$s}\n"; echo $s; if(@$logfile)@file_put_contents($logfile, file_get_contents($logfile) . $s); } function handshake($connect) { $info = array(); $line = fgets($connect); $header = explode(' ', $line); $info['method'] = $header[0]; $info['uri'] = $header[1]; while ($line = rtrim(fgets($connect))) { if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) { $info[$matches[1]] = $matches[2]; } else { break; } } $address = explode(':', stream_socket_get_name($connect, true)); $info['ip'] = $address[0]; $info['port'] = $address[1]; if (empty($info['Sec-WebSocket-Key'])) { return false; } $SecWebSocketAccept = base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept:".$SecWebSocketAccept."\r\n\r\n"; fwrite($connect, $upgrade); return $info; } function encode($payload, $type = 'text', $masked = false) { $frameHead = array(); $payloadLength = strlen($payload); switch ($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if ($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 255 : 127; for ($i = 0; $i < 8; $i++) { $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 if ($frameHead[2] > 127) { return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)'); } } elseif ($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; } // convert frame-head to string: foreach (array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if ($masked === true) { // generate a random mask: $mask = array(); for ($i = 0; $i < 4; $i++) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: for ($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame; } function decode($data) { $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; // unmasked frame is received: if (!$isMasked) { return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)'); } switch ($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; case 2: $decodedData['type'] = 'binary'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)'); } if ($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; } elseif ($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $tmp = ''; for ($i = 0; $i < 8; $i++) { $tmp .= sprintf('%08b', ord($data[$i + 2])); } $dataLength = bindec($tmp) + $payloadOffset; unset($tmp); } else { $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = $payloadLength + $payloadOffset; } /** * We have to check for large frames here. socket_recv cuts at 1024 bytes * so if websocket-frame is > 1024 bytes we have to wait until whole * data is transferd. */ if (strlen($data) < $dataLength) { return false; } if ($isMasked) { for ($i = $payloadOffset; $i < $dataLength; $i++) { $j = $i - $payloadOffset; if (isset($data[$i])) { $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData; }
Данный сервер отправляет всем клиентам время в формате HH:ii:ss как только изменилось time(), т.е. каждую секунду.
В скрипте осуществлена поддержка SSL сертификатов от Letsencrypt (второй параметр — домен на который выдан сертификат):
/usr/src/socket.php 8800 conture.by
Если при запуске опустить параметры, то сервер запуститься на дефолтовом порту 8888 в обычном незащищённом режиме (ws://)
Скрипт вебсокета сервера должен работать постоянно (в режиме демона), поэтому делаем для этого слот в /etc/systemd/system/socket.service:
[Unit] Description=socket After=network.target [Service] Type=simple ExecStart=/usr/src/socket.php 8800 conture.by [Install] WantedBy=multi-user.target Alias=socket.service
Далее включаем слот systemd:
systemctl enable socket systemctl start socket