PHP로 HTTP 서버 구현하기 - 02 - Stream wrapper

연재글 전체 보기


Stream wrapper class

시행착오를 최대한 줄이기 위해 앞에서 소개했던 HTTPServer부터 분석을 해보기로 했다.

composer.json에 있는 php-cgi binary에 대한 의존성 때문인데(HTTPServer requires the php-cgi binary), 이 놈을 왜 넣었을까 궁금했다.

이 서버는 4개의 파일로 구성됐다.

  • cgistream.php
  • httprequest.php
  • httpresponse.php
  • httpserver.php

http가 prefix로 붙은 파일은 뭐하는 놈인지 이름만 봐도 알 것 같다. cgistream.php을 열어봤다.

오래전에 만든 클래스라서 좀 옛스러운 맛이 있다.

CGIStream class 주석

주석부터 살펴볼까?

/*
* CGIStream is a PHP stream wrapper (http://www.php.net/manual/en/class.streamwrapper.php)
* that wraps the stdout pipe from a CGI process. It buffers the output until the CGI process is
* complete, and then rewrites some HTTP headers (Content-Length, Status, Server) and sets the HTTP status code
* before returning the output stream from fread().
*
* This allows the server to be notified via stream_select() when the CGI output is ready, rather than waiting
* until the CGI process completes.
*/

CGIStream is a PHP stream wrapper that wraps the stdout pipe from a CGI process.

CGI process로부터 흘러나오는 stdout stream을 wrapping한다. - 이건 뭐 해석한 것도 아니고 안 한 것도 아니고…

참고로, 앞으로 스트림과 stream을 섞어서 쓸텐데(한글 혹은 영어로), 여기엔 큰 의미를 부여하지 말고 그냥 읽으시면 된다.

Stream wrapper class

php.net에 들어가보면,

Allows you to implement your own protocol handlers and streams for use with all the other filesystem functions (such as fopen(), fread() etc.).

나만의 프로토콜 핸들러와 fopen, fread같은 filesystem 함수를 사용할 수 있는 스트림을 구현할 수 있다고 한다.

php.net의 VariableStream을 만드는 예제부터 알아봐야겠다.

우선 커스텀 stream wrapper를 어떻게 사용하는지부터.

Using stream wrapper

<?php
$existed = in_array("var", stream_get_wrappers());
if ($existed) {
stream_wrapper_unregister("var");
}
stream_wrapper_register("var", "VariableStream");

“var”로 이미 등록된 wrapper가 있다면 먼저 제거한다.

stream_get_wrappers는 등록된 stream wrapper 배열을 리턴하는데, 보통 이런 리스트가 나올 것이다.

Array
(
[0] => https
[1] => ftps
[2] => compress.zlib
[3] => compress.bzip2
[4] => php
[5] => file
[6] => glob
[7] => data
[8] => http
[9] => ftp
[10] => phar
[11] => zip
)

이렇게 unregister하더라도 built-in wrapper일 경우는 나중에 stream_wrapper_restore로 복원할 수 있다.

stream_wrapper_register함수의 두번째 인자는 첫번째 인자(protocol)에 해당하는 스트림을 처리할 클래스명이다.

이런 클래스를 어떻게 구현할 것인지는 잠시 후에 다룬다.

$myvar = "";

$fp = fopen("var://myvar", "r+");

fwrite($fp, "line1\n");
fwrite($fp, "line2\n");
fwrite($fp, "line3\n");

위에서 언급한 대로 stream wrapper를 구현하면 fopen, fread같은 filesystem 함수를 사용할 수 있다.

fopen 함수로 스트림을 r+(초기화가 없는 read+write) 모드로 열고, 몇 줄 써준다.

URL에는 아까 stream_wrapper_register함수에 첫번째 인자로 등록한 protocol을 URL scheme으로 사용하고 입력받을 myvar라는 전역 변수를 사용한다.

물론 URL을 어떻게 쓰고 처리할 지는 wrapper 구현에 달려있다.

rewind($fp);
while (!feof($fp)) {
echo fgets($fp);
}
fclose($fp);
var_dump($myvar);

파일 포인터를 맨 앞으로 돌리고, 파일의 내용을 한줄 한줄 써준다.

if ($existed) {
stream_wrapper_restore("var");
}

다 썼으면 초기화 한다. 만약,

stream_wrapper_register("var", "VariableStream");
stream_wrapper_unregister("var");
stream_wrapper_restore("var");

이러면 뭐 달라질까? 방금 등록한 var wrapper는 built-in이 아니기 때문에 restore해도 아무 의미 없다.

사용하는 방법은 특별할 게 없으므로, VariableStream이란 클래스를 어떻게 구현하면 될지 살펴보자.

Stream wrapper

Stream wrapper class는 넓은 의미의 인터페이스지만,

implements 할 수 있는 interface는 아니다. 실제 존재하는 클래스가 아니라 어떻게 동작하는 지를 보여주기 위한 prototype일 뿐이다.

Note:
This is NOT a real class, only a prototype of how a class defining its own protocol should be.

php.net에 누가 댓글로 interface로 만들어 올렸지만, 그건 거 쓰지 말라는 댓글도 보인다(필요하지 않은 메소드도 무조건 구현해놔야 하기 때문에).

필요한 메소드만 구현하는 게 정석이라고 보면 되겠다.

댓글로 올라온 VariableStream 클래스를 참고해서 위 스크립트가 돌아갈 만한 간단한 커스텀 wrapper를 실행해봤다.

몇가지 문법 오류와 변수명을 좀 더 알기 쉽게 바꿨다.

class VariableStream
{
private $position;
private $varname;

public function stream_open($path, $mode, $options, &$opened_path)
{
$url = parse_url($path);
$this->varname = $url["host"];
$this->position = 0;
return true;
}

public function stream_read($count)
{
$position =& $this->position;
$ret = substr($GLOBALS[$this->varname], $position, $count);
$position += strlen($ret);
return $ret;
}

public function stream_write($data)
{
$variable =& $GLOBALS[$this->varname];
$len = strlen($data);
$position =& $this->position;
$variable = substr($variable, 0, $position) . $data . substr($variable, $position += $len);
return $len;
}

public function stream_tell()
{
return $this->position;
}

public function stream_eof()
{
return $this->position >= strlen($GLOBALS[$this->varname]);
}

public function stream_seek($offset, $whence)
{
$len = strlen($GLOBALS[$this->varname]);
$position =& $this->position;
switch ($whence) {
case SEEK_SET:
$newPos = $offset;
break;
case SEEK_CUR:
$newPos = $position + $offset;
break;
case SEEK_END:
$newPos = $len + $offset;
break;
default:
return false;
}
$ret = ($newPos >= 0 && $newPos <= $len);
if ($ret) {
$position = $newPos;
}
return $ret;
}
}

여기선 stream_open, stream_read, stream_write, stream_tell, stream_eof, stream_seek 만을 구현했는데,

그 (가상) 인터페이스의 많은 메소드 중에 뭘 구현해야 할 지 어떻게 알 수 있을까?

그건 사용처에서 어떤 filesystem 함수를 쓰는가에 따라 다르다.

이 예제에서 stream_tell 함수를 구현하지 않았다면, 아래와 같은 warning이 뜰 것이다.

PHP Warning:  rewind(): VariableStream::stream_tell is not implemented! in .....

공용 클래스라면 좀 더 충실히 구현해야겠지만, 이 예제는 이만하면 됐다.

많이 돌아왔는데, 맨 처음에 언급한 CGIStream 클래스로 돌아갈 차례다(이제 겨우 CGIStream 클래스의 주석 한줄 읽었을 뿐이다).

다음 글에 계속..