개발 꿀팁/PHP

PHPIO 프로그래밍 epoll 구현 방안

Jammie 2022. 9. 13. 11:39
반응형

EPOll이란 무엇이며, PHP는 어떻게 epoll 모델의 IO를 구현할 수 있을까?

 

epoll은 리눅스 커널이 대량의 파일 기술자를 처리하기 위해 개량한 poll로 리눅스 언더멀티플렉싱 IO 인터페이스 select/poll의 증강 버전으로, 프로그램이 대량 동시 접속에서 소량만 활성화되었을 경우의 시스템 CPU 활용률을 현저히 높인다.또 다른 이유는 이벤트를 획득할 때 전체 수신된 설명자 집합을 통과하지 않고 커널 IO 이벤트에 의해 비동기적으로 깨어난 설명자 집합을 통과하면 되기 때문입니다.epoll은 select/poll과 같은 IO 이벤트의 수평 트리거(Level Triggered) 외에 에지 트리거(Edge Triggered)를 제공하므로 사용자 공간 프로그램이 IO 상태를 캐시할 수 있고 epoll_wait/epoll_pwait 호출이 감소하여 응용 프로그램의 효율성이 향상됩니다

 

1: PHP Socket 구현 IO(select 모드)
현 단계에서 php 네이티브 방법은 epoll 모델을 사용할 방법이 없는 네이티브 구현이다.Select 모델만 사용할 수 있으며, 주요 단계는 다음과 같습니다

  		 //1:ip: port. socket 수신기 만들기
        $socket = stream_socket_server($socket_address);
        //2:차단되지 않음으로 설정
        stream_set_blocking($socket , 0);s
        
        //3:socket 전체 프로세스가 여기에 차단되어 계속 읽을 수 있는 이벤트를 수신합니다
        //여기서 매개 변수는 모두 참조 전달이며, 함수에서 전달 값을 변경합니다. 첫 번째는 수신할 수 있는 소켓 배열, 두 번째는 쓰기 가능한 소켓, 인터페이스 세부 정보 참조https://www.php.net/manual/zh/function.stream-select.php
             while(true) {
               stream_select($sockets, [],[], 60);
                 foreach ($sockets as $index => $socket) {
                 //TODO 데이터가 있는 socket
                 
                 }
             }

장점: 간편하고 빠르고, 경량이며, 의존 라이브러리를 인용하지 않아도 됨
단점: select IO 모델만 사용할 수 있음단일 스레드는 최대 1024개의 파일만 열 수 있으며 Select 모델은 성능이 떨어져 동시성이 높은 시나리오에는 적용되지 않는다.

완전한 데모

<?php

class SocketServer{
    //소켓 듣기
    protected $socket = NULL;

    //모든 소켓 연결
    protected $sockets = array();

    //연결 이벤트 콜백
    public $onConnect = NULL;

    //단선사건 콜백
    public $onClose = NULL;

    //메시지 이벤트 콜백 받기
    public $onMessage = NULL;

    public function __construct($socket_address) {
        //소켓 듣기 만들기
        $this->socket = stream_socket_server($socket_address);

        //차단되지 않음으로 설정
        stream_set_blocking($this->socket, 0);

        //allSockets에 socket 감청하기
        $this->sockets[(int)$this->socket] = $this->socket;
    }

    public function run() {
        while(true) {
            //쓰기 가능한 이벤트와 외부 데이터 이벤트를 수신하지 않음
            $write = $except = array();
            //모든 소켓 이벤트 듣기
            $read = $this->sockets;
            //모든 프로세스가 여기에 막혀 있으며, 지속적으로 읽을 수 있는 이벤트를 수신합니다
            //여기서 매개 변수는 모두 참조 전달입니다. 함수에서 전달 값이 바뀝니다
            stream_select($read, $write, $except, 60);

            //읽을 수 있는 모든 이벤트 처리
            foreach ($read as $index => $socket) {
                //Socket을 수신하는 경우, 여기에서는 새 연결이 있음을 나타냅니다
                if ($socket === $this->socket) {
                    //stream_socket_accept에서 새 연결 가져오기
                    $new_conn_socket = stream_socket_accept($socket);

                    if ($this->onConnect) {
                        //연결 이벤트의 콜백을 트리거하고 현재 연결을 콜백 함수에 전달합니다
                        call_user_func($this->onConnect, $socket);
                    }
                    //sream_select가 읽을 수 있는 이벤트를 들을 수 있도록 이 socket 연결을 기록합니다
                    $this->sockets[(int)$new_conn_socket] = $new_conn_socket;
                } else
                    //가독 이벤트가 수신 socket이 아닌 경우 해당 클라이언트에서 데이터가 전송되었음을 나타냅니다
                {
                    //연결에서 데이터 읽기
                    $buffer = fread($socket, 65535);
                    //데이터가 비어 있으면 클라이언트의 연결이 끊어졌음을 나타냅니다
                    if ('' === $buffer || false === $buffer) {
                        //onClose 콜백 시도
                        if ($this->onClose) {
                            call_user_func($this->onClose, $socket);
                        }
                        fclose($socket);
                        //소켓 연결을 닫고 allSockets에서 삭제
                        unset($this->sockets[(int)$socket]);
                        continue;
                    }
                    //메시지를 읽은 정상적인 연결을 나타내며, 백홀 함수에 넘겨줍니다
                    if ($this->onMessage) {
                        call_user_func($this->onMessage, $socket, $buffer);
                    }
                }
            }
        }
    }
}

$server = new SocketServer('tcp://0.0.0.0:9501');

$server->onConnect = function ($conn) {
    echo 'connect';
};
$server->onClose = function ($conn) {
    echo 'close';
};
$server->onMessage = function ($conn, $message) {
  $http_resonse = "HTTP/1.1 200 OK\r\n";
    $http_resonse .= "Connection: keep-alive\r\n";
    $http_resonse .= "Server: php socket server\r\n";
    $http_resonse .= "Content-length: 11\r\n\r\n";
    $http_resonse .= "hello world";
    fwrite($conn, $http_resonse);
};

$server->run();

둘: event 확장을 사용하여 epoll 구현하기(또는 Libeven 확장, 둘 중 선택)
php event는 하나의 사건 라이브러리로, 시중에 상용하는 각종 IO 다중화 기술에 대한 통일된 패키지로, 통용된다.epollio 모델 통신 가능

확장설치

# event 다운로드
wget https://pecl.php.net/get/event-3.0.3.tgz
 
# 파일 압축 풀기
tar -xf event-3.0.3.tgz
 
# 디렉터리에 들어가다
cd event-3.0.3
 
# phpize 실행
/www/server/php/72/bin/phpize
 
./configure --with-php-config=/www/server/php/72/bin/php-config
 
# 설치하다.
make && make install
#php.ini 설정 변경

extension = /www/server/php/72/lib/php/extensions/no-debug-non-zts-20170718/event.so

데모 사용

<?php
$s_host = '0.0.0.0';
$i_port = 9501;
$r_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_set_option( $r_listen_socket, SOL_SOCKET, SO_REUSEADDR, 1 );
socket_bind( $r_listen_socket, $s_host, $i_port );
socket_listen( $r_listen_socket );
// $listen_socket을 비차단 IO로 설정
socket_set_nonblock( $r_listen_socket );

$a_event_array  = array();
$a_client_array = array();

// event-base 만들기
$o_event_base  = new EventBase();
$s_method_name = $o_event_base->getMethod();
if ( 'epoll' != $s_method_name ) {
    exit( "not epoll" );
}

function read_callback( $r_connection_socket, $i_event_flag, $o_event_base ) {
    $s_content = socket_read( $r_connection_socket, 1024 );
    echo "받아들이다:".$s_content;
    // 이 클라이언트 연결 소켓에 읽기 이벤트 추가
    // 이 클라이언트 연결 socket이 쓰기 가능 조건을 충족하면 우리는 socket에 데이터를 쓸 수 있다
    global $a_event_array;
    global $a_client_array;
    $o_write_event = new Event( $o_event_base, $r_connection_socket, Event::WRITE | Event::PERSIST, 'write_callback', array(
        'content' => $s_content,
    ) );
    $o_write_event->add();
    $a_event_array[ intval( $r_connection_socket ) ]['write'] = $o_write_event;
}
function write_callback( $r_connection_socket, $i_event_flag, $a_data ) {
    global $a_event_array;
    global $a_client_array;
    $s_content = $a_data['content'];
    foreach( $a_client_array as $r_target_socket ) {
        if ( intval( $r_target_socket ) != intval( $r_connection_socket ) ) {
            socket_write( $r_target_socket, $s_content, strlen( $s_content ) );
        }
    }
    $o_event = $a_event_array[ intval( $r_connection_socket ) ]['write'];
    $o_event->del();
    unset( $a_event_array[ intval( $r_connection_socket ) ]['write'] );
}
function accept_callback( $r_listen_socket, $i_event_flag, $o_event_base ) {
    global $a_event_array;
    global $a_client_array;
    // socket_accept연결 받기,새로운 것을 생성하다socket,클라이언트 연결socket
    $r_connection_socket = socket_accept( $r_listen_socket );
    $a_client_array[]    = $r_connection_socket;
    // 이 클라이언트 연결 소켓에 읽기 이벤트 추가
    //즉, 클라이언트 연결에서 메시지를 읽어야 합니다
    $o_read_event = new Event( $o_event_base, $r_connection_socket, Event::READ | Event::PERSIST, 'read_callback', $o_event_base );
    $o_read_event->add();
    $a_event_array[ intval( $r_connection_socket ) ]['read'] = $o_read_event;
}

// $listen_socket에 읽기 이벤트 추가
// 왜 사건을 읽습니까?
// $listen_socket에서 발생하는 이벤트는 클라이언트 연결 설정이기 때문입니다
// 그래서 사건을 읽어야 한다
$o_event = new Event( $o_event_base, $r_listen_socket, Event::READ | Event::PERSIST, 'accept_callback', $o_event_base );
$o_event->add();
//$a_event_array[] = $o_event;
$o_event_base->loop();

장점: epoll 모델을 지원하며, 동시 성능이 좋고, 충분히 유연하여 쓰기 프레임에 적합하다.
단점: 문서, 튜토리얼이 비교적 적으며, 스스로 더듬는 경우가 많고, 포장도가 높지 않으며, 프로젝트에서 사용하기 위해서는 2가 필요하다.서브 패키지

셋:Swoole

Swoole은 tcp, http, webscoket 등 각종 통신 서비스를 고도로 캡슐화하여 사용이 간편하고, 내부적으로는 모두 epoll 호출을 사용한다

$server = new Swoole\Server('127.0.0.1', 9503);

$server->on('start', function ($server) {
    echo "TCP Server is started at tcp://127.0.0.1:9503\n";
});

$server->on('connect', function ($server, $fd){
    echo "connection open: {$fd}\n";
});

$server->on('receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, "Swoole: {$data}");
});

$server->on('close', function ($server, $fd) {
    echo "connection close: {$fd}\n";
});

$server->start();

장점 : epoll 모델 사용 가능, 효율성, 포장도, 사용 편의성, 문서 완성
단점:프레임워크가 방대하고, 기존의 FPM프로그래밍과는 완전히 다르며, 많은 조작이 프로그램 이상을 일으키기 쉽다.입문 문턱이 비교적 높다

요약: 현 단계에서 php 네이티브는 epoll 모델의 IO 호출을 지원하지 않으며, 3자 토폴로지를 통해 가능하다.Event를 전개하여 epoll 콜을 구현하거나 swoole을 이용하여 직접 서비스를 생성하면 됩니다

 

반응형