본문 바로가기

공부/컴퓨터

PHP 소스 수정을 통한 파일 업로드 진행바 구현

반응형

PHP 소스 수정을 통한 파일 업로드 진행바 구현 - PHP 4.20용


4월 22일, PHP 4.20이 정식 발표되었습니다.

php.net에서는 PHP 4.20의 주요한 변화점을 다음과 같이 공지하였습니다.

 


  • External variables (from the environment, the HTTP request, cookies or the web server) are no longer registered as global variables
  • Overhaul of the sockets extension
  • Highly improved performance with file uploads
  • The satellite and mailparse extensions were moved to PECL and are no longer bundled with the official PHP release
  • The posix extension has been cleaned up
  • iconv handling has been improved
  • Output buffering support, which was introduced in PHP 4.1.0 has been stabilized
  • Improved performance and stability of the domxml extension
  • New multibyte regular expression support
  • LOTS of fixes and new functions

제가 주목한 부분은 3번째입니다. 파일 업로드의 퍼포먼스가 매우 많이 향상되었다는군요.

얼마나 좋아졌는지 테스트하기 위해, 로컬 네트워크상에서 200메가 정도의 파일을 업로드 해봤습니다(10M 랜입니다).

4.12버전에서 200K로 시작해 60K의 속도를 끝으로 업로드되던 것이, 4.20에서는 400K 이상의 속도로 순식간에 업로드되더군요(제 서버의 경우입니다. 서버의 메모리 크기와 HDD 속도에 따라 이 수치는 바뀔수 있습니다). 또한, 서버의 CPU점유율도 더이상 100%를 차지하지 않더군요.

서버의 사양과 대역폭만 확실하다면, 이제 PHP로 메이저급의 자료실을 만드는 것도 가능하게 되겠군요!!!!

 

그런데, PHP 4.12에서 기껏 만들었던 업로드 진행바도 제대로 동작하지 않더군요. 왠지모를 의무감(?)에 급히 소스를 뒤져 새로 4.20에 알맞는 수정방법을 찾아 이렇게 공개합니다.

PHP 4.20에서의 POST 전송방식을 이해하시려면, DeadFire님의 CGI 강좌(http://deadfire.hihome.com)도 꼭 읽어보시길 추천하며, 소스 수정을 이해하시려면 제가 4.12용으로 작성하였던 글을 읽어보시길 추천합니다. 어떻게 PHP 4.20의 소스를 변경해야 하는지 궁금하신 분은 1/2번은 스킵하시고 3번 내용만 읽어보시면 됩니다.

 

 

1. PHP 4.20 - POST 전송 처리 방식의 변화

 

어찌하든간에, PHP의 소스를 들여다 봤더니, POST 데이터를 받는 구조 자체가 완전히 바뀌었더군요.

PHP 4.20과 4.12와의 POST 데이터를 받는 방식의 변화를 비교해보겠습니다.

 

PHP 4.12의 경우


  1. 클라이언트에서 POST 전송이 들어오면 어느정도 메모리를 확보한 후, POST 데이터를 받기 시작한다.

  2. POST 데이터의 크기가 현재 할당한 메모리의 크기보다 크면, 메모리를 추가로 조금 더 확보한다. POST 전송이 끝날때까지 이 과정을 반복한다 (전송되는 POST데이터의 크기가 500메가라면 서버에도 500메가의 메모리가 할당된다).

  3. POST 전송이 끝나면, 메모리상에 저장된 POST 데이터들을 스캔하여 분석하기 시작한다.

  4. 이 아닌 POST 데이터들은 각각 메모리를 확보하여 PHP의 스크립트에서 사용할 수 있는 변수로서 저장한다.

  5. 에 해당하는 POST 데이터들은 임시폴더에 파일로 저장하고, 관련된 정보들(파일크기, 원래이름, 저장된 임시이름 등)을 PHP의 스크립트에서 사용할 수 있는 변수로 할당한다.

  6. 클라이언트가 리퀘스트한 스크립트 파일을 열어 번역하고, 실행시킨다.

PHP 4.20의 경우


  1. 클라이언트에서 POST 전송이 들어오면 어느정도 버퍼를 확보한 후, POST 데이터를 받기 시작한다.

  2. 현재 전송받는 데이터가 이 아닌 데이터일 경우 : POST 데이터의 크기가 현재 할당한 버퍼의 크기보다 크면, 메모리를 추가로 더 확보한후 계속 받는다. 현재의 데이터의 전송이 끝나면, 방금전 받은 POST 데이터를 PHP의 스크립트에서 사용할 수 있는 변수로서 저장한다(4.12와 동일한 과정). 그런후 버퍼를 비우고 다음 데이터를 받기 시작한다.

  3. 현재 전송받는 데이터가 에 해당하는 POST 데이터일 경우 : 임시파일을 일단 하나 생성하여, 쓰기모드에 들어간다. 그리고 POST 데이터를 받기 시작한다. 그러다 POST 데이터가 버퍼를 다 채우면, 현재 읽어들인 부분을 임시파일의 뒷부분에 써 넣는다. 그리고 할당한 버퍼를 비운 후, 클라이언트에서의 POST 데이터를 계속 받는다. 이 과정을 다음 데이터(혹은 전송의 끝)가 나올때까지 계속 반복한다(조금읽고 쓰고, 조금읽고 쓰고, 조금읽고 쓰고....). 다음 데이터(혹은 전송의 끝)가 나오면, 임시파일을 닫는다.

  4. 모든 데이터들을 다 받을때까지 2번과 3번과정을 반복한다.

  5. 클라이언트가 리퀘스트한 스크립트 파일을 열어 번역하고, 실행시킨다.

음. 비교가 되시나요? 4.12는 모든 전송을 다 받은 후, 그걸 메모리상에서 번역하여 알맞은 형태로 저장합니다. 그에 비해 4.12는 지금 받는 것의 형태가 file인지 아니면 일반 데이터인지 구분하여 데이터를 전송받으면서 동시에 처리(메모리에 저장, 혹은 파일에 저장)를 합니다.

 

따라서 4.12처럼 500메가가 전송될 시 500메가의 메모리를 할당하지 않아도 됩니다. 4.20에서의 기본 버퍼 메모리는 5KByte입니다(rfc1867.c의 #define FILLUNIT (1024 * 5)). 아무리 업로드되는 파일이 크더라도 5Kbyte 이상의 버퍼를 사용하지 않습니다(물론 이것저것 생각하면 이보다는 많이 쓰겠지만요^^). 이렇게 구조가 바뀜으로서 PHP 4.20에서는 기가막힌 퍼포먼스의 향상을 이루어 냈습니다. 제가 생각한 이유는 다음과 같습니다.

 

PHP 4.12에서 버퍼의 크기가 다 찼을때 추가시키는 버퍼의 크기는 4KByte정도였습니다(SAPI.h의 #define SAPI_POST_BLOCK_SIZE 4000). 100메가를 전송받을 경우, 100*1024/4 = 25600번의 메모리 요청을 했어야 합니다. 아마도 가상메모리도 할당해야겠죠! 그러다 보니 전송의 후반부가 될수록 속도는 점점 더 줄어들 것입니다. 10메가 파일 전송시 시작은 200K, 전송 후반부엔 60K....

4.20에서는 에 해당하는 데이터가 있으면 데이터 들어오는대로 죽죽 파일로 저장하는 것입니다. HDD의 성능만 받쳐준다면, 4.20의 전송속도가 더 빠를 것 같습니다. 또한, 메모리 부족도 생기지 않겠죠? 하지만... 단점도 있으니....

 

 

2. PHP 4.20 POST 관련 소스 분석 - SAPI_POST_HANDLER_FUNC()


4.12에서 POST 리퀘스트를 받는 부분이 SAPI.c 부분에 존재했었는데, 4.20에서는 그 부분이 rfc1867.c 파일의 SAPI_POST_HANDLER_FUNC()에서 처리하도록 변경되었습니다. 4.12에서 rfc1867.c 파일이 맡은 역할은 3/4/5번 역할이였습니다. 즉, 메모리를 스캔하여 각각의 을 분리하는 역할이였죠. 4.20에서는 전송받는 역할과 스캔하여 분리하는 핸들러 역할까지 모두 통합되어 rfc1867.c 파일에서 처리하게 되었습니다(파일명이 rfc1867이지만, 첨부파일이 없더라도 모든 POST 데이터는 이곳에서 처리합니다. rfc1867이란 form 을 통한 파일 전송 규약이죠).

 

4.12 부분을 설명할때는 POST되는 데이터를 받는 루프만 설명하면 되었지만, 4.20에서는 POST 데이터를 전송받는 부분과 그것을 핸들링 하는 부분이 통합되었기에, 설명하는 내용이 다분히 CGI에 대한 내용이 될수밖에 없을듯 합니다. 따분하고 지루한 이야기...

다음 그림은 SAPI_POST_HANDLER_FUNC()의 소스입니다. 소스가 너무나도 길기에 대부분을 생략하고 주요 부분만 간추렸습니다.

 



 

원래는 200여줄 정도의 소스이지만, 중요도가 떨어지는 부분은 생략하여 70줄로 만들었습니다.

분석을 해봅시다. 실제로는 더블 버퍼링을 사용하지만, 설명의 편의성을 위해 간략화하여 설명하겠습니다.

 

7번째 줄 이전에, 버퍼를 포함한 각종 변수들을 초기화합니다(중요도가 떨어져 생략!).

 

7번째 줄에 코드된 while (!multipart_buffer_eof(mbuff TSRMLS_CC)) 을 통해 함수 전체가 무한루프를 돌게 됩니다. 

multipart_buffer_eof() 함수는, 클라이언트에서 POST된 전송을 버퍼의 크기 만큼 읽어들이고, 만약 방금전 읽어들인 POST 전송의 크기가 0바이트면 1을, 0바이트 보다 크면 0을 리턴합니다. 따라서, 위의 while구문은, 메인버퍼크기 만큼 POST 데이터를 읽어라! POST 데이터를 전송받았으면 밑에 while 내부를 실행시키고, 아니면 탈출하라라는 뜻입니다.

사실상 이 while문은, 전송된 의 갯수만큼 루프를 돌게 됩니다. 한번 루프돌때마다 한개씩을 처리하는 것입니다. 이제 루프 안으로 들어갑니다.

 

생략되어 있는 9번째 줄에서 10번째 줄 사이에서는, 세컨드버퍼를 정의하고, 버퍼의 내용 중에서 헤더만을 세컨드버퍼로 옮긴 후, 클라이언트에서의 전송을 잠시 멈추게 합니다(더블 버퍼링은 복잡하므로 이후 간략화하여 설명하겠습니다). 데이터 반입 잠시 금지! 버퍼에 저장되어있는 내용은 현재 에 해당하는 헤더입니다.

각각의 마다 헤더가 붙어서 옵니다. 이 헤더에 태그의 name, type 등의 정보가 들어있죠(자세한 정보는 Deadfire님의 CGI 강좌를 참고하세요).

만약 태그가 였다면, 헤더에는

 

Content-Disposition: form-data; name="UploadFile";

filename="c:\test.txt"

Content-Type: text/plain

 

식으로 태그의 정보가 담겨 있습니다.

 

11번째 줄의 if에서는, 헤더에서 Content-Disposition이라는 부분의 값을 받아옵니다.

만약 이 값이 없다면? 그것은 FORM에서 전송시 식으로 값을 가진 INPUT이 전송된 것이 아니라 그냥 이렇게만 적어놓은 태그란 뜻이겠죠. 이름도 없고, 타입도 없고.... 그런 태그라면 while의 처음으로 돌아가 다음 태그에 대한 처리를 시작하겠죠. 만약 제대로 된 태그라면, if문 안으로 들어가게 됩니다.

 

13번째 줄의 생략된 부분에서는, 위의 Content-Disposition: 뒤의 값에서 "filename="이라는 구문이 있는지, 그 값이 있는지 여부를 확인하는 것입니다.

 

16번째 줄의 if 구문은 이 아닌 일반적인 나 <TEXTAREA>, 등에 해당하는 들을 처리하는 구문입니다.

위에서 찾은 "filename="의 값이 없으면, 이 아닙니다. 이런 들은 value 데이터가 아무리 길어봤자 몇십/몇백 Kbyte일 것입니다. 이런 값들은, 모든 value값이 다 전송될때까지 POST 데이터를 전송받습니다(버퍼가 모자라면 버퍼를 계속 더 할당받더군요). 데이터 반입 개시!!!! 다 전송받으면, 일반적인 스크립트에서 사용할 변수로서 설정합니다. 그런후 버퍼를 비우고, contine;를 통해 while 문의 처음으로 돌아가게 됩니다. 즉, 다음 태그를 찾는 것이죠.

따라서. 20번째 줄 이하부터는 인 태그들만 실행되게 됩니다.

 

23번째 줄의 if는 php.ini에서 파일 업로드를 불가능하게 하였다면, while 루프의 처음으로 돌아가서 다음 을 받게 됩니다.

 

28번째 줄에서는 에 대한 파일을 받기 위해 임시폴더에 임시파일을 하나 만듭니다. 여기까지 내려왔다면 제대로 된 이란 뜻일테니깐요. fclose()가 없는 것에 주목해주세요.

 

41번째 while 문에서 다시 POST된 데이터를 받기 시작합니다. 이 while문을 파일저장while이라고 합시다. 데이터 반입 개시!!! 이 데이터는 메모리에 저장되는 것이 아니라 버퍼링 후 임시파일로 저장합니다. 그것도 파일을 fopen(), fclose()처럼 열고 닫고를 반복하는것이 아니라 그냥 죽죽 써 나갑니다. 해당 파일의 데이터를 다 전송받게 되면 파일저장while 루프를 탈출하게 됩니다.

파일저장while문을 탈출하게 되면, 곧장 63번째 줄에서 fclose()를 통해 파일을 닫습니다.

 

41번째 줄 이후는, 현재 전송된 파일명, 파일크기 등에 대한 변수를 설정하는 것입니다.

그런후, while의 처음으로 돌아가서 다음 을 전송받게 됩니다.

이런식으로 모든 태그를 다 처리하게 됩니다.  하나마다 while() 문 한번씩 돌기~!

 

모든 것을 다 처리했을때는 함수를 종료합니다. 그런데, 70번째 줄에서 return; 이 아니라 SAFE_RETURN으로 함수를 리턴합니다.

이 함수 내부에서는 return; 이 하나도 없고 전부 SAFE_RETURN을 사용하는군요. SAFT_RETURN는 rfc1867.c파일의 제일 위에 정의되어 있습니다.

 



 

풀어줘야 할 메모리를 전부 풀어준 후 return;시키는 것이군요....

 

분석이 끝났습니다. 너무 복잡하군요... 동작원리를 정확히 확인하시고 싶으시면 꼭 DeadFire님의 CGI 강좌를 읽어보시고, 원 소스를 살펴보시길 바랍니다.

어찌하든 동작원리를 알았으니 이제, 소스를 수정합시다.

 

 

3. 업로드 진행바를 위한 소스 수정

 

업로드 진행바가 구현되는 원리는 제가 4.12버전때 말씀드렸던 것과 동일합니다. 4.12때와 위치만 바뀌었을 뿐이지 구현 방법은 똑같습니다. 자세한 내용은 4.12버전에 대해 제가 쓴 글을 참고해주시기 바랍니다.

이 글에서는 4.20에서 제가 수정했던 부분만을 간략히 보여드리도록 하겠습니다. 모든 수정은 rfc1867.c 에서 행해졌습니다.

 

먼저, 제가 사용할 변수들을 설정해야겠죠.

SAPI_POST_HANDLER_FUNC()의 제일 첫머리에 제가 사용할 변수들을 정의하였습니다.

 



 

POST 데이터들을 받기 전에, GET방식으로 전달한 값들중에서 "UPLOAD_CHECK_ID=어쩌구저쩌구"라는 값이 있는지 확인해봐야 합니다. 확인하여 존재한다면, 그 어쩌구저쩌구란 값을 strCheckFileLocation에 저장합니다. 저는 각각의 데이터들을 받기 직전, 즉 while 문 바로 앞에 이 부분을 추가하였습니다.

 



 

다음은, 어쩌구저쩌구.Progress 파일을 생성하여 그 파일안에 현재의 진행 정도를 적어놓는 부분입니다. 저는 POST된 데이터를 임시파일에 써넣는 부분에 이런 기능을 추가시켰습니다. 파일저장while 내부중 아무곳에나 넣어도 되겠죠. 다시한번 말씀드리지만, 리눅스 서버를 사용하신다면 공유메모리를 사용하는 방식으로 만들 것을 추천합니다. 혹은, 진행정도와 ID를 저장/출력하는 업로드 모듈을 만드셔서 이곳에서 억세스하도록 하는 것도 괜찮을 것 같습니다.

 



 

마지막으로 제가 설정하였던 변수의 메모리를 해제해야 합니다. 서버에서의 메모리 릭은 끔찍한 결과를 초래합니다. 그런데, SAPI_POST_HANDLER_FUNC() 함수 내부에서 return되는 곳이 너무 많군요. 이곳저곳 써넣으면 귀찮으니, 정의된 SAFE_RETURN을 약간 변화시킵시다(SAPI_POST_HANDLER_FUNC()의 모든 return은 SAFE_RETURN을 통해 행해집니다). rfc1867.c 파일의 제일 앞머리에 있는 #define SAFE_RETURN 부분에 한줄만 더 추가시킵시다.

 



 

이제 컴파일하시고, 만들어진 exe, dll등을 사용하시면 됩니다. 업로드진행바의 사용 방법은 4.12때의 글에 나와있습니다.

4.12때와 마찬가지로 브라우져1에서는 파일업로드를 진행하고, 브라우져2에서는 업로드 진행과정을 관찰하시면 될 것입니다.

 

 

4. 실제모습

 

4.20에 위의 업로드 진행바를 사용할때 의외의 문제점이 있었습니다.

분명히 PHP 4.20의 업로드 성능은 매우 좋아졌습니다. 하지만 그로 인해 업로드진행정도를 제대로 전달하지 못할 지경이더군요.

브라우져 1에서 업로드를 하고 있는동안, 브라우져 2는 계속 서버와 교신하면서 브라우저1의 업로드 현황을 가져와 화면에 보여줘야 합니다. 그런데, 브라우져1에서 계속 쭉쭉쭉 데이터를 폭주하듯 업로드하다보니, 브라우져2와 서버가 교신하기가 너무 버거워하는 것이였습니다--; 엔진바꾼 86이 계기판 때문에 못달리는 것과 비슷한 상황이더군요.

1초에 한번씩 갱신되어야 하는 업로드 진행바가 10초에 한번, 5초에 한번씩 갱신되더군요. 또 진행바를 사용했을 시 업로드 속도가 약간 줄어들더군요...

이것은 웹서버에서 클라이언트와의 전송 속도 제한을 걸던가, 서버의 스토리지를 15000RPM 치타HDD로 RAID 5를 걸던가, 혹은 서버가 리눅스라면 파일에 업로드 현황을 저장하는 것이 아니라 공유메모리를 사용하게 하는 등의 방식을 통해 개선할 수 있을 것이라 생각됩니다(제 서버 환경은 윈도우 2000 프로페셔널입니다).

 

어찌하던간에, 다 끝났습니다. 제 홈페이지 게시판에서 로컬네트워크로 파일을 업로드하는 모습입니다. adsl의 속도가 느려서인지, 외부에서 접속하신 분들께는 딜레이가 별로 없는 듯 하더군요.

 



 

다른 많은 분들께서 PHP 내부를 더 찾아봐 주셔서 더 효율적이고 좋은 방법이 개발되었으면 좋겠습니다.

Win32 모듈용 php4ts.dll 파일과 패치된 소스 파일은 제 홈 게시판에 걸어놓도록 하겠습니다.

수정해야 할 부분, 개선해야 할 부분이 있거나 제 글에 문의하실 내용이 있으시면 wndproc@shinbiro.com으로 메일 주시거나 http://wndproc.d2g.com에 접속해 주시기 바랍니다.

긴 글 읽어주셔서 감사합니다.



http://phpschool.com/bbs2/inc_view.html?id=6802&code=tnt2
반응형