socket практика использования

fedot

Новичок
Всем доброго здоровья!

Задумался над созданием онлайн игры (простой, по типу крестики нолики, два игрока). И планирую использовать вэб сокеты для мгновенной передачи данных. Но тут у меня возникли вопросы по поводу использования socket-ов:

1. в интернете пишут что php очень плохо справляется с нагрузкой. Где-то встречал информацию что нормально работает с 50 пользователями на линии, а потом начинает тормозить (сбрасывать соединения). У кого есть опыт боевого тестирования? Сколько соединений php может нормально обслуживать? За основу можно взять самый дешевый vps хостинг с такими параметрами: оперативка 1 гб. 2 проц. ssd 10 гб. интернет канал 200 мб./сек.

2. как перезапустить файл server.php (где крутятся все соединения) если он завершит процесс?

3. на клиент мы передаем адрес соединения с сервером: ws://sait.ru:8080/socket/server.php, но ведь по этому же адресу можно запустить скрипт из адресной строки браузера. Я так и запускаю этот файл на локалке. То есть получается что любой желающий может открыть эту ссылку в браузере и перезапустить скрипт?

4. если предполагать что скрипт слушает соединения по протоколу ws, то злоумышленник может отправить по этому протоколу 1 милл. запросов и остановить выполнение скрипта (вырубить игру, чат)?
 

WMix

герр M:)ller
Партнер клуба
1. на половину правда, php не самый лучший выбор для web-socket, но выдержит в 100 раз больше
2. Ctr+C [enter] php server.php?
3. чет я не понял, одно дело сервер (порт), другое адрес, не путай, не выставляй скрипт сервера в публик
4. ну да, и на http://sait.ru/ тоже, и больше миллиона, называется ddos,
зубов бояться в рот не давать.
 

fedot

Новичок
2. Ctr+C [enter] php server.php?
3. чет я не понял, одно дело сервер (порт), другое адрес, не путай, не выставляй скрипт сервера в публик
4. ну да, и на http://sait.ru/ тоже, и больше миллиона, называется ddos,
2. ну руками запустить это понятно, я имею ввиду как узнать что он (скрипт) отвалился и как перезапустить его в автоматическом режиме?
3. то есть в .htaccess прописать правило запрета на этот файл: sait.ru/socket/server.php ? Или в самом скрипте что-то прописать еще?
4. ну к примеру рядовой сайт вполне справится со 100 000 запросов в сутки, а вот скрипт с сокетами отвалится как только число подключений приблизится к этой цифре. Следовательно ddos-ить сокеты на много выгоднее и проще чем сам сайт?
 

WMix

герр M:)ller
Партнер клуба
2. поставь supervisor или чтонить подобное
3. вообще вынеси за пределы public
4. а с 100.000 в минуту?
 

fedot

Новичок
3. вообще вынеси за пределы public
4. а с 100.000 в минуту?
3. в таком случае ссылка на клиенте будет выглядеть так: ws://sait.ru:8080/ ?
4. в таком случае возможно ли ограничить подключение по ip, скажем 10 подключений с одного ip (именно по протоколу ws)? Эту проверку воткнуть в файл server.php или это другими средствами реализуется?
 

WMix

герр M:)ller
Партнер клуба
3. какая хочешь, но и так можно - корень
4. "скажем 10 подключений" - ты хочешь 10 websockets иметь? но в любом случае да, сам сервис может быть спрятан за другим сервисом типа proxy (nginx к примеру) и там есть свои инструменты
 

fixxxer

К.О.
Партнер клуба
1) забудь про shared hosting, так ничего не выйдет, возьми vps, хоть самую дешевую за 200 рублей. никакой апач тут не понадобится, скрипт будет работать сам по себе.
2) http://socketo.me/ - смотри пример чата, онлайн игра принципиально не отличается
3) один из extension-ов из этого списка среди тех, которые начинаются на Ext (через stream_select работать будет, но сожрет весь проц очень быстро)
4) запускай демоном, хоть через тот же systemd
 

fedot

Новичок
Спасибо за ответы! Но эта технология не как не ложится в мою голову.
Вот у меня есть файл который запускает и обслуживает соккеты:
PHP:
set_time_limit(0);
define('PORT', '8099');

require_once __DIR__ . '/classes.php';

$connect = new Connect();

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket, 0, PORT);

socket_listen($socket);

$clientSocketArray = [$socket];

while(true){
  
    $newSocketArray = $clientSocketArray;
    $nullA = [];
    socket_select($newSocketArray, $nullA, $nullA, 0, 10);
  
    if(in_array($socket, $newSocketArray)){
      
        $newSocket = socket_accept($socket);
        $clientSocketArray[] = $newSocket;
        $header = socket_read($newSocket, 1024);
        $connect->sendHeaders($header, $newSocket);
      
        socket_getpeername($newSocket, $client_ip);
        $createMessageConnect = $connect->createMessageConnect($client_ip);
        $connect->sendMessage($createMessageConnect, $clientSocketArray);
      
        $newSocketArrayIndex = array_search($socket, $newSocketArray);
        unset($newSocketArray[$newSocketArrayIndex]);
    }
  
    foreach($newSocketArray as $newSocketArrayResourse){
      
        //1
        while(socket_recv($newSocketArrayResourse, $socketDataBufer, 1024, 0) >= 1){
            $socketMessage = $connect->unseal($socketDataBufer);
            $messageArr = json_decode($socketMessage);
          
            $userClient = $messageArr->user;
            $messageClient = $messageArr->text;
            $pKey = $messageArr->pKey;
          
            if($userClient && $messageClient && $pKey){
                $chatMessage = $connect->createChatMessage($userClient, $messageClient, $pKey);
                $connect->sendMessage($chatMessage, $clientSocketArray);
            }else if($pKey){
              
            }
          
            break 2;
        }
      
        //2
        $socketData = @socket_read($newSocketArrayResourse, 1024, PHP_NORMAL_READ);
        if($socketData === false){
            socket_getpeername($newSocketArrayResourse, $client_ip);
            $disconnect = $connect->createMessageDisconnect($client_ip);
            $connect->sendMessage($disconnect, $clientSocketArray);
          
            $newSocketArrayIndex = array_search($newSocketArrayResourse, $clientSocketArray);
            unset($clientSocketArray[$newSocketArrayIndex]);
        }
      
    }
  
}

socket_close($socket);

и файл с классом который обрабатывает сообщения:
PHP:
class Connect
{
    public function sendHeaders($headersTxt, $newSocket){
        $headers = [];
        $arrHeaders = explode("\r\n", $headersTxt);
      
        foreach($arrHeaders as $value){
            $arrValue = explode(": ", trim($value));
            if(isset($arrValue[0],$arrValue[1])){
                $headers[$arrValue[0]] = $arrValue[1];
            }
        }
      
        $key = $headers['Sec-WebSocket-Key'];
        $sKey = base64_encode(pack('H*', sha1($key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
      
        $responseHeaders = "HTTP/1.1 101 Switching Protocols\r\n".
            "Upgrade: websocket\r\n".
            "Connection: Upgrade\r\n".
            "Sec-WebSocket-Accept: $sKey\r\n\r\n";
          
        socket_write($newSocket, $responseHeaders);
    }
  
    public function createMessageConnect($client_ip){
        $message = "Client ip: ".$client_ip.", connect!";
        $messageArray = [
            'message' => $message,
            'type' => 'createMessageConnect'
        ];
        $ask = $this->seal(json_encode($messageArray));
        return $ask;
    }
  
    public function createMessageDisconnect($client_ip){
        $message = "Client ip: ".$client_ip.", disconnected!";
        $messageArray = [
            'message' => $message,
            'type' => 'createMessageDisconnect'
        ];
        $ask = $this->seal(json_encode($messageArray));
        return $ask;
    }
  
    public function sendMessage($message, $clientSocketArray){
      
        $messageLenght = strlen($message);
      
        foreach($clientSocketArray as $clientSocket){
            @socket_write($clientSocket, $message, $messageLenght);
        }
      
        return true;
    }
  
    public function seal($data){
      
        $b1 = 0x81;
        $lenght = strlen($data);
        $header = '';
      
        if($lenght <= 125){
            $header = pack('CC', $b1, $lenght);
        }elseif($lenght > 125 && $lenght < 65536){
            $header = pack('CCn', $b1, 126, $lenght);
        }else{
            $header = pack('CCNN', $b1, 127, $lenght);
        }
      
        return $header.$data;
    }
  
    public function unseal($data){
      
        $lenght = ord($data[1]) & 127;
      
        if($lenght == 126){
            $mask = substr($data, 4, 4);
            $txt = substr($data, 8);
        }elseif($lenght == 127){
            $mask = substr($data, 10, 4);
            $txt = substr($data, 14);
        }else{
            $mask = substr($data, 2, 4);
            $txt = substr($data, 6);
        }
      
        $socketStr = '';
      
        for($i=0; $i<strlen($txt); ++$i){
            $socketStr .= $txt[$i] ^ $mask[$i%4];
        }
      
        return $socketStr;
    }
  
    public function createChatMessage($user, $message, $pKey){
        $messageNew = "User : $user, message: $message, key: $pKey";
        $messageArray = [
            'message' => $messageNew,
            'type' => 'createChatMessage'
        ];
      
        $ask = $this->seal(json_encode($messageArray));
      
        return $ask;
    }
  
}

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

Но в моем случае ситуация совсем другая. За одним столом могут сидеть от 2 до 8 игроков. Значит соединения тоже нужно разбивать на группы. Плюс ко всему, там же еще идет запись в базу (ходов например), чтение из базы (кому можно играть а кому нет). В общем плотное общение с БД. И как все это реализовать?

Всю логику разбиение на группы и работы с БД тоже в файл server.php запихнуть? Если да, то не завалит ли он БД запросами? И вообще, как правильно на группы разбивать?
 

fixxxer

К.О.
Партнер клуба
Это плохой пример из интернета. Впрочем, если у тебя 10-20 игроков одновременно - предел, сойдет.

Еще раз даю ссылку:

 

fixxxer

К.О.
Партнер клуба
За одним столом могут сидеть от 2 до 8 игроков. Значит соединения тоже нужно разбивать на группы.
Ну и отлично, у тебя есть некая сущность - игра, какой-нибудь class Game. Есть коллекция игр, есть коллекция соединений которые class Collection. Ну вот и свяжи их в обе две стороны. $game->connections[], ну и какой-нибудь SplObjectStorage для обратной связи.

Плюс ко всему, там же еще идет запись в базу (ходов например), чтение из базы (кому можно играть а кому нет). В общем плотное общение с БД.
С базой надо работать асинхронно, конечно же.
Хотя не вижу никакой необходимости именно писать в базу: писать можно в очередь, а разгребать уже отдельным процессом.
Читать - ну, да, наверное. Хотя мне redis-а на все про все хватило для аналогичной задачи.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
ограничение будет не у PHP, а по возможностям сервера на количество одновременных соединений по сети
Не совсем согласен с такой формулировкой (я-то понимаю, что ты имеешь ввиду).
Ключевой момент тут все же в модели обработки соединений. С подходом "по процессу на соединение" возможности сервера закончатся не на количестве соединений (вон они в бэклоге ждут), а на количестве процессов. С подходом socket_select возможности сервера закончатся на бессмысленной итерации 100500 неактивных соединений по 100 раз в секунду, и так далее.

вместо ратчета порекомендую https://amphp.org/
Да, он прикольно выглядит (с генераторами - полноценный аналог async-await), но сам не использовал.
 

fedot

Новичок
Конечно библиотеки и готовые решения хорошо, но если не понимать что происходит, то с таким подходом в программировании далеко не уедешь. А я пока что даже в основах не разобрался. Хотелось все реализовать на нативном php и mysql.
 

fixxxer

К.О.
Партнер клуба
Не вопрос, бери pecl/event и реализуй. Ну и вот, ознакомься (это все плюс-минус актуально до сих пор - конкретные примеры устарели, но подходы - нет).

Просто смысла особого нет, тут надо понимать принципы, а этому в данном случае библиотека не помешает, а наоборот поможет.

php тут, кстати, вообще не имеет особого значения: на любом языке будет то же самое по принципу действия.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@fixxxer если поставить фразу про ограничение в конец после ссылки на amphp - будет логиченее :)

Хотелось все реализовать на нативном php и mysql.
почитай про OSI-модель
"нативный" php использует не сокеты, а HTTP - а принцип HTTP в том, что соединение с браузером разрывается после передачи ответа,
и сервер не может уведомить клиента по какому-то событию - для этого нужен, например, pushpin, вебсокеты
 
Сверху