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을 이용하여 직접 서비스를 생성하면 됩니다
'개발 꿀팁 > PHP' 카테고리의 다른 글
composer를 이용한 자체 프로젝트 구축 (1) | 2022.09.13 |
---|---|
PHP 손글씨 HTTP 프로토콜 (0) | 2022.09.13 |
php 지정한 시간대가 맞는지 확인하는 방법 (0) | 2022.09.12 |
php 2차원 배열의 여러 1차원 배열의 합성 방법 (0) | 2022.09.12 |
php 배열에서 지정된 여러 열을 반환하는 방법 (0) | 2022.09.12 |