삽질하는플머

php-xmlrpc 에서 시간함수 문제

탐구생활/WEB 관련

상황은 이렇다. 


델파이로 만든 XMLRPC 클라이언트에서 "시간값"을 인자로 PHP로 만든 XMLRPC 서버의 함수를 실행하고

이 값을 MySQL의 타임스탬프 필드에 저장하자. 


xml-rpc 의 시간표시는 iso 8601 에 따라 다음과 같이 표시된다.

  • 2011-08-24T00:00:00

xmlrpc.inc 에는 이 형식과 유닉스 타임스탬프 사이의 변환을 위해 iso8601_encode, iso8601_decode 함수가 정의되어있다. 
이렇게 변환한 유닉스 타임스탬프값을 MySQL에 집어넣기 위해 MySQL 내장함수인 FROM_UNIXTIME() 을 사용해 봤는데
클라에서 전송한 시간보다 +9 시간 더 나오는 문제가 발생. iso8681_decode의 두번째 인자에 1을 주어 UTC로 풀어도 마찬가지.

RPC 테스트중인 윈도의 PHP는 현재시간이 UTC로 설정되어있다. 그런데 시스템시간은 한국. 그래서 생기는 문제인 듯. 
실제로 다음과 같은 코드로 타임존을 설정하면 제대로 처리된다.


date_default_timezone_set("Asia/Seoul");


NAS, 또는 우분투의 PHP에서 echo date_default_timezone_get(); 을 찍어보면 Asia/Seoul 이 잘 출력되는 것을 보아 윈도용 WAMP의 PHP에서 시스템의 설정을 읽지 못하는 문제로 보인다. 그렇다고 코드에 저걸 무조건 박아넣기도 골룸.


date로 문자열 꾸미기도 시도해 봄. 


1 $tt = iso8601_decode($ExpireDate);
2 $ts = date("Y-m-d G:i:s", $tt);

하지만 iso8601_decode 내부에서 gmmkttime, 또는 mktime 을 실행시키면서 타임존 문제가 생김. 


뭐가 어쨌건 시스템의 날짜 설정과 무관하게 델파이에서 전송한 시간값을 MySQL에 입력하기만 하면 되므로...
iso8601_decode 의 앞부분을 흉내내 이렇게 만들어 봄. 

1 function iso8601_time_to_mysql($idate) {
2     if (preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/', $idate, $regs)) {
3         return sprintf("%s-%s-%s %s:%s:%s", $regs[1], $regs[2], $regs[3], $regs[4], $regs[5], $regs[6]);
4     } else
5         return NULL;
6 }    


요는, php의 시간변환 함수를 사용하지 말자는 것. 
비슷한 요령으로 역변환도 만들어 둠. 

1 function mysql_time_to_iso8601($idate) {
2     if (preg_match('/([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/', $idate, $regs)) {
3         return sprintf("%s%s%sT%s:%s:%s", $regs[1], $regs[2], $regs[3], $regs[4], $regs[5], $regs[6]);
4     } else
5         return NULL;
6 }


이제 델파이에서 의도한 시간값이 PHP를 거쳐 MySQL로 제대로 꽂힌다. .



여담으로, date('Z') 로 얻어낸 값에는 현재 설정된 타임존과 UTC 사이의 분단위 시간차이가 들어있다.
GetTimeZoneInformation() 으로 얻어낸 TimeZhoeBias 값과 동일. 그냥 까먹을까봐 적어둠. 




xmlrpc-epi 로 만든 RPC 서버에서 압축전송된 요청 다루기

탐구생활/WEB 관련
델파이와 PHP 에서 XLMRPC 를 가지고 노는 이야기는 오래전에 위키에 끄적인 적이 있는데... 지금은 날아가버렸다. 
백업된 문서 찾아다 블로그에 정리해야겠다. (... 고 생각한지 벌써 일년이 훌쩍... ㅠㅠ) 

xmlrpc-epi 는 예전에 살펴본 php-xmlrpc 와 기능면에서 거의 같으면서도 C로 만들어진 확장이라 속도가 좀 더 빠르다. 
사용법도 비슷해서 include("xmlrpc.inc"); 를 include("xmlrpc_emu.inc"); 라고 끝에 "_emu" 만 붙여주면 된다. 
무엇보다 대부분의 PHP 배포판에 기본적으로 포함되어있다는 것이 장점이다. 심지어는 시놀로지의 NAS에도 들어있다. 
게다가 나는 팔랑귀이므로 뭐가 조금이라도 좋다고 하면 써봐야 직성이 풀린다. 

 다만 무슨 일인지 우분투 10.04에서는 빠져있는데, 아래와 같이 설치하고 아파치를 재구동하면 올라온다.  (예전에는 들어 있었는데...)

sudo apt-get install php5-xmlrpc



WAMP 에서는 트레이를 클릭하고 PHP -> PHP Extensions -> php_xmlrpc 확장을 선택하면 된다. 





xmlrpc

core library version xmlrpc-epi v. 0.51
php extension version 0.51
author Dan Libby
homepage http://xmlrpc-epi.sourceforge.net
open sourced by Epinions.com



http://xmlrpc-epi.sourceforge.net/ 에서 xmlrpc-epi-php-0.51.tar.gz 를 다운받아 압축을 풀고 util 디렉토리를 include 경로에 포함시키면 사용준비는 완료. 


 

PHP+MySQL+xmlrpc-epi 로 만든 XMLRPC 기반의 데이터 서버와 델파이로 정보를 주고 받는 중인데, DB값의 특성상 키와 값으로 쌍을 이루는 "구조체" 형식을 선호하다 보니 전송 데이터가 상당히 커지게 되었고, 이거 필드에서 제대로 써먹을 수 있을지 슬쩍 겁이나기 시작했다. 

일단 받는 정보부터 압축해보자. PHP.INI 를 다음과 같이 설정하고
 

zlib.output_compression = on
zlib.output_compression_level = 9 



zlib
........

DirectiveLocal ValueMaster Value
zlib.output_compression On On
zlib.output_compression_level 9 9
zlib.output_handler no value no value



RPC 함수 호출시 HTTP 헤더의  Accept-encoding 에 "gzip,deflate" 라고 적어주면 gzip으로 압축된 정보가 날아온다. deflate로 날아온 정보는 ZLib로 바로 풀리고 gzip의 경우는 
DelphiZLib 의 ZLibExGZ 유니트를 사용하면 되므로 일단 한 숨 돌렸는데...
저장하기 위해 서버로 보낼 정보도 만만치 않게 크기 때문에 마찬가지로 압축을 해주기로 하면서 고민이 시작된다.



HTTP 헤더의 Content-Encoding 에 deflate, 또는 gzip 을 적어주고 XML을 압축한 뒤 서버를 호출하니 다음과 같은 에러가 나온다. 

32700: parse error. not well formed. 
error occurred at line 1, column 1, byte index 0



데이터가 요사스러워 첫줄 첫번째 컬럼 제일 앞바이트부터 에러가 난다고 툴툴거리고 있다.
저 에러는 C확장 내부에서 내는 것으로서 한마디로 압축된 그대로 넘겨받았다는 이야기. 


흠... 설정에 output_xxx 만 있을 때 부터 알아봤어야 하는데... 들어오는 정보는 직접 풀어줘야하나보다. 


서버 객체가 정의된 xmlrpcs_emu.inc 파일의 177 라인을 살펴보니 전달된 HTTP_RAW_POST_DATA 를 날것 그대로 던져주고 있다. 
여기에 클라에서 전달받은 Content-encoding 값으로 압축여부를 판단해 압축을 풀어주는 코드를 추가해보자. 

    // public. service the xmlrpc request
    function service() {
       Header("Content-type: text/xml; Content-length: " . strlen($payload));
 
       global $HTTP_RAW_POST_DATA;
       $data=$HTTP_RAW_POST_DATA;
 

      // 데이터가 압축되어있는지 살핀다. 압축되어있다면 풀자. 
         if(isset($_SERVER['HTTP_CONTENT_ENCODING']))
             $content_encoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
         else
             $content_encoding = '';
 
         if ($content_encoding == 'gzip' && function_exists('gzinflate') && $degzdata = @gzinflate(substr($data, 10))) {
             //echo 'GZip decompress!';
             $data = $degzdata;
         } else
         if ($content_encoding == 'deflate' && function_exists('gzuncompress') && $degzdata = @gzuncompress($data)) {
             //echo 'Deflate decompress!';
             $data = $degzdata;
         }



        // call server
       echo xmlrpc_server_call_method($this->xmlrpc_server, $data, $this->dmap, 
                                      array(output_type => "xml", version => "xmlrpc", encoding => "UTF-8"));
    }



녹색으로 둘러친 부분이 추가한 내용이다. 이제 함수호출 정보를 gzip, 또는 deflate 로 압축해 던져도 제대로 반응하게 된다.  

XML은 텍스트로 된 정보이므로 압축률이 높다. 크기가 커질수록 효율은 더욱 좋아진다.
3천바이트 가량의 XML이 1/5인 600바이트 정도로 줄어들어 왔다갔다 하는 것을 보면 답답했던 속이 뻥~ 뚫릴 것이다. 



ps. php-xmlrpc 에는 압축 전송된 요청을 풀어주는 코드가 이미 들어있다.
     따라서 이 고민은 "똑같은 PHP에서 XMLRPC 사용인데 xmlrpc-epi 는 왜 안되는거야!!" 하는 사람에게만 유효하다. 



PHP 컴파일러 HipHop for PHP from facebook

이런저런잡다구리/LinuxHeaven
PHP코드를 C++코드로 변환, g++로 빌드할 수 있게 하는 물건. 
결과물로 libevent 를 사용한 독립 웹서버 실행파일이 떨어진다. 
페이스북에서 쓰이고 있으며 성능 개선이 상당하다고 한다. 
모든 PHP 코드가 다 동작하는 것은 아니겠지만 관심이 무럭무럭!!

일단 홈페이지. 
https://github.com/facebook/hiphop-php/wiki/

한글로 된 소개글
http://enzine.tistory.com/entry/HipHop-for-PHP-%EB%8D%94-%EB%B9%A0%EB%A5%B8-PHP%EB%A5%BC-%EC%9C%84%ED%95%B4

KLDP의 새소식
http://kldp.org/node/112325

먼저 써보신 분의 의견. 
http://code.p-ark.co.kr/167 





레드마인을 위해 설치해 둔 우분투 10.04에 깔아보자. 9.10에 설치하는 참고링크는 여기.
https://github.com/facebook/hiphop-php/wiki/Building-and-Installing-on-Ubuntu-9.10
10.10용은 여기. (명색이 LTS인 10.04만 빠져있네...) 
https://github.com/facebook/hiphop-php/wiki/Building-and-Installing-on-Ubuntu-10.10


먼저 관련프로그램 설치. 

sudo apt-get install git-core cmake g++ libboost-dev libmysqlclient-dev libxml2-dev libmcrypt-dev libicu-dev openssl binutils-dev libcap-dev libgd2-xpm-dev zlib1g-dev libtbb-dev libonig-dev libpcre3-dev autoconf libtool libcurl4-openssl-dev libboost-system-dev libboost-program-options-dev libboost-filesystem-dev wget memcached libreadline-dev libncurses-dev libbz2-dev libc-client2007e-dev



9.10과 마찬가지로 10.04의 libmemcached 는 버전이 0.31이므로 쓸 수 없다. 0.39 이상이 필요하므로 따로 빌드. 

HipHop 소스 얻기. 

mkdir hiphop
cd hiphop
git clone git://github.com/facebook/hiphop-php.git
cd hiphop-php
export CMAKE_PREFIX_PATH=`/bin/pwd`/../
export HPHP_HOME=`/bin/pwd`
export HPHP_LIB=`/bin/pwd`/bin
git submodule init
git submodule update
cd ..



LIBEVENT 빌드.

wget http://www.monkey.org/~provos/libevent-1.4.13-stable.tar.gz
tar -xzvf libevent-1.4.13-stable.tar.gz
cd libevent-1.4.13-stable
cp ../hiphop-php/src/third_party/libevent-1.4.13.fb-changes.diff .
patch -p1 < libevent-1.4.13.fb-changes.diff
./configure --prefix=$CMAKE_PREFIX_PATH
make
make install
cd ..



ICU4 빌드

wget http://download.icu-project.org/files/icu4c/4.2.1/icu4c-4_2_1-src.tgz
tar -xvzf icu4c-4_2_1-src.tgz
cd icu/source
./configure --prefix=$CMAKE_PREFIX_PATH
make
make install
cd ../../




LIBCURL 빌드. 시스템 시간 잘 맞추지 않으면 ./configure 가 실패한다네. 
중간에 패치가 필요하므로 지정된 버전을 쓸 것. 

wget http://curl.haxx.se/download/curl-7.20.0.tar.gz
tar -xvzf curl-7.20.0.tar.gz
cd curl-7.20.0
cp ../hiphop-php/src/third_party/libcurl.fb-changes.diff .
patch -p1 < libcurl.fb-changes.diff
./configure --prefix=$CMAKE_PREFIX_PATH
make
make install
cd ..



LIBMEMCACHED 빌드. 현재 최신버전인 0.49로

wget http://launchpad.net/libmemcached/1.0/0.49/+download/libmemcached-0.49.tar.gz
tar -zxvf libmemcached-0.49.tar.gz
cd libmemcached-0.49
./configure --prefix=$CMAKE_PREFIX_PATH
make
make install
cd ..



HipHop 빌드. 걸어놓고 밥먹고 오자. 

cd hiphop-php
cmake . <- 점 주의!
make



hphp 바이너리가 src/hphp 에 생성된다. 

  



테스트. 사용법은 일단 여기.
https://github.com/facebook/hiphop-php/wiki/Running-HipHop


먼저 hphp를 체크아웃 한 디렉토리로 이동. (내 경우 ~/hiphop/hiphop-php)

export HPHP_HOME=`pwd`
export HPHP_LIB=`pwd`/bin



우분투에서는 추가로 이렇게 해주라는군.

export CMAKE_PREFIX_PATH=`/bin/pwd`/../



예전 GD 가지고 놀 때 코드를 한번 컴파일 해보자. 
적당한 디렉토리에 예제 php 파일 생성. 


ip-test.php
  

<?php
$img_number = imagecreate(140,25);
$backcolor = imagecolorallocate($img_number,255,255,255);
$textcolor = imagecolorallocate($img_number,50,104,28);

imagefill($img_number,0,0,$backcolor);
$number = "IP - $_SERVER[REMOTE_ADDR]";

Imagestring($img_number,3,0,5,$number,$textcolor);

header("Content-type: image/jpeg");
imagejpeg($img_number);
?>



컴파일

$HPHP_HOME/src/hphp/hphp ip-test.php -k 1 --log=3 --force=1 




임시 디렉토리 (내 경우에는 /tmp/hphp_TFludl) 에 실행파일이 생성된다. 
8080포트에서 구동시키는 예. 80포트로 띄우려면 관리자권한(sudo)이 필요하다. 

/tmp/hphp_TFludl/program -m server -p 8080


 
해당 서버에 접속해보자. http://my-ubuntu:8080/ip-test.php
결과는 짜잔~~





GD 테스트를 조금 더 해본다. 
프리타입이 동작하는지도 확인해보자. 윈도의 gulim.ttc 를 /tmp 에 복사해넣자. 

gd-test.php

<?php
// 이미지 생성
$image_test = ImageCreate(200,150);

// 색의 설정
$grey = ImageColorAllocate($image_test,200,200,200);
$blue = ImageColorAllocate($image_test,0,0,255);

imagefill($image_test,10,0,$grey);

// 내장폰트 사용 예
Imagestring($image_test,3, 25,50,"hahaha",$blue);

// 프리타입으로 TTF 폰트 사용 예
Imagefttext($image_test,10, 0, 25, 100, $blue, "/tmp/gulim.ttc", "우하히");

header("Content-type: image/png");
imagepng($image_test);
ImageDestroy($image_test);
?>



HipHop은 컴파일 이전에 이 PHP가 제대로 동작할지를 미리 확인해볼 수 있는 hphpi 를 제공한다. 

$HPHP_HOME/src/hphpi/hphpi  -m server -p 8080



현재 디렉토리를 기준으로 서버가 동작한다. 매번 빌드하는 것도 시간이 걸리는 일이므로, 이걸 쓰면 지금 작성중인 코드가 HipHop에서 잘 동작하는지 확인해 볼 수 있다. 

접속해보면 잘 처리되는데... 



이 PHP 파일에 include, 또는 include_once 를 써서 다른 PHP 유니트를 포함시키면 이미지 출력이 제대로 되지 않는다. 
이미지 출력을 위해 사용되는 header(), imagepng() 등의 함수가 아예 동작하지 않는 듯 하다. 
현 시점에서 GD 관련해서는 단일 유니트만 사용 가능할 듯. 진심으로 아쉬운 부분... 




예전 게임서버는 DB 입출력을 담당하는 에이전트 서버를 통해 각종 정보를 저장해 왔었다. 
이 경우 운영도 귀찮고 뭐 하나 바꾸려 할때 만져야 할 부분이 참 많다. 

때문에 로그 저장 서버는 상대적으로 편한 PHP + xmlrpc 를 써서 만들었었고
나중에 게임서버를 새로 만든다면 DB 입출력은 이 방식으로 하리라 마음먹었었는데...

HipHop을 이용하면 그 부분도 네이티브 바이너리로 굴릴 수 있다는 이야기. 
퍼블리셔에 텍스트로 된 PHP 파일을 그대로 전달해야 하는 찝찝함도 더불어 해결. 

다만 생성된 실행파일의 크기가 약 20메가 정도로 큰 편인데, UPX로 압축하면 7메가 정도로 줄어든다. 

# sudo apt-get install upx-ucl
# upx program


서버에서 실행파일의 크기야 별 의미가 없겠지만... 

언제 시간날 때 VCL4PHP 를 빌드해봐야겠다. 
 



 
2011.8.6. XMLRPC 를 HipHop for PHP로 구동시켜보자. 
 
디렉토리 구성은 다음과 같이.


루트
test.php : 에코함수인 EchoFunc() 가 구현된 서버.
  • lib : php-xmlrpc 의 lib

먼저 제대로 동작하는지 테스트 해보자. 
 

$HPHP_HOME/src/hphpi/hphpi -m server -p 8080


 
접속해보면 자알~~ 동작한다.
이제 빌드해보자.

먼저 웹루트에서 다음과같이 파일목록을 만든다.

find . -name "*.php" > files.list



files.list 의 내용은 다음과 같다. 

./test.php
./lib/compat/var_export.php
./lib/compat/is_scalar.php
./lib/compat/is_callable.php
./lib/compat/is_a.php
./lib/compat/array_key_exists.php
./lib/compat/version_compare.php



test.php 의 내용은 다음과 같다. 

<?php

include("lib/xmlrpc.inc");
include("lib/xmlrpcs.inc");

$xmlrpc_internalencoding="UTF-8";

/****************************************************************
 테스트. 에코함수.
****************************************************************/

$EchoFunc_sig = array(array($xmlrpcString, $xmlrpcString));
$EchoFunc_doc = "에코함수!!";

function EchoFunc($m) {
        // 전달된 XML에서 인자값을 뽑아낸다.
        $s=$m->getParam(0);
        $msg = $s->scalarval();
        return new xmlrpcresp(new xmlrpcval("니가 웹서버에 보낸 메시지: " . $msg, "string"));
}

/****************************************************************
 서버 구동
****************************************************************/
$s = new xmlrpc_server(array(

        "EchoFunc" => array(
                "function" => "EchoFunc",
                "signature" => $EchoFunc_sig,
                "docstring" => $EchoFunc_doc
                ),

        ));
?>

 

힙합으로 빌드. 

$HPHP_HOME/src/hphp/hphp --input-list=files.list -k 1 --log=3 \
--include-path="." --force=1 --cluster-count=50 \
-v "AllDynamic=true" -v "AllVolatile=true"



/tmp/hphp_ahNFdu 폴더가 생성됨.

80포트를 쓰지 않는다면 sudo 권한은 필요없음. 다음과 같이 구동. 
 

/tmp/hphp_ahNFdu/program -m server -p 8080


아주아주아주 잘 동작하네~~

신기한건 test.php 코드내에서 포함시킨 "xmlrpc.inc", "xmlrpcs.inc" 유니트가 files.list 내에 없어도 제대로 동작한다는 것... 
include() 로 포함시킬 때 함께 들어가게 되는건지... 흠...