내가 다니는 학교 POSTECH은 기숙사 학교기 때문에, 배달 음식을 시켜먹는 일이 잦다.
근데 요즘 돈도 없었고 먹을 일도 잘 없어 배달음식을 자주 안시켜먹었더니, 뭐가 맛있고 뭐가 괜찮은지 모르는 상황에 이르렀다.
그래서 랜덤으로 배달메뉴를 고르는 프로그램을 만들어야겠다는 결론에 도달했다.
루비로 짰는데, CGI로 짤려고 했더니 매번 호출때마다 파싱해 오는게 너무 느려서 standalone web server로 만들었다.
웹서버 코드는 웹서핑 해서 나온 코드를 대충 때려박아 넣었다.
핵심인 배달업체 페이지 분석에서 학교 홈페이지가 공개되면 여러모로 곤란해질듯 하여 그 부분은 X로 마스킹 했으니, 그냥 켜면 절대 돌아가지 않는다.
요즘은 인터넷 홍수 시대라고 해도 과언이 아닐 정도로 엄청난 양의 정보들이 인터넷을 통해 유통되고 있다. 최초의 인터넷(정확히는 www:World Wide Web)은 단순히 발행된 정보를 독자에게 전해주는 단방향 커뮤니케이션이었지만, CGI라고 하는 요술방망이가 등장한 이후 사용자가 서버에게 단순히 요청만 하는 것이 아니라 정보를 제공해주고 그 제공된 정보를 서버가 다시 처리해 돌려주는 신비한 일도 발생하기 시작했다.
지금이야 php다, RoR이다, django다 하는 더 이상한 놈들에게 자리를 빼앗기거나 대체, 또는 그것들을 위한 단순한 backend 수준으로 그 위상이 격하된 CGI이지만 처음 나왔을 때는 상당하고도 신선한 충격이었고, 덕분에 Perl이라는 언어는 원래 계획됐던 보고, 시스템 관리용 언어에서 벗어나서 굉장히 넓은 지평을 갖게 되었다. 한 때 우리나라에서 유행했던 소위 “설치형 웹게임”이란 놈들은 거의 다 이 Perl로 짜여지기도 했다.
Perl은 시스템 관리용으로도 쓰였기 때문에 웬만한 머신에는 다 설치되어 있었고, 따라서 CGI를 쓰는 데는 큰 문제가 없었지만 가끔씩 Perl이 설치되지 않았거나 Perl을 사용할 수 없는 웹서버가 있었고, 또 Perl이란 언어 자체가 내부적으론 컴파일링을 하지만 원시 코드(source code)를 그대로 입력받는 언어였기에 필연적으로(하지만 지금은 전혀 상관없는) 속도가 느려지기 마련이어서, 가끔 바이너리로 된 CGI가 배포되기도 했었다.
하지만 이제 와서는 사실, Perl이 안 깔린 머신도 없고 웬만한 기능은 다 Perl, python, ruby 등의 언어로 바인딩이 제공되고 있으며 저런 것을 쓰더라도 속도 문제에 있어서 전혀 신경을 쓰지 않아도 될 정도의 시대인지라, C로 CGI를 만들겠다는 것은 정말 뻘짓이 아닐 수 없다. 그럼에도 불구하고 C로 CGI를 만들어야 하는 이유가 있다.
C는 모든 언어의 기본이다. 이 말은 C가 정말 기초적이기 때문에 이 것을 배워야 다른 언어로 CGI를 짤 수 있다는 되도안한 소리가 아니다. 무슨 뜻이냐면, 특히나 Linux의 경우, 대부분의 라이브러리들이 1차적으로 C 언어로 된 인터페이스를 갖는다. 거기에 추가적으로 필요하다면 각 언어용 바인딩이 제공되어 설치하여 쓰게 되어 있다. 따라서, Perl이나 python, ruby용 바인딩이 설치되어 있지 않은 어떤 라이브러리를 사용하고 싶다면, 그 바인딩을 구해서 사용할 수 없는 경우엔 C로 짜는 것이 대안이 될 수 있다는 것이다. 이 '사용할 수 없는 경우'라는 것은 상당히 많은 경우를 포함하는데, C와의 호환이 필요한 라이브러리의 경우 C로 컴파일해야 하는 경우가 자주 발생한다. 이 때 가끔씩, 뭔가 알 수 없는 이유로 컴파일이 안되는 경우가 발생하기도 하고, 컴파일을 무사히 마치더라도 적절한 위치에 넣을 수 있는 권한이 없어 실패할 수도 있다. 기본적으로 각 언어별로 제공되는 확장기능 관리 기능을 통해 설치하려고 해도 보통은 권한이 없어서 그것도 여의치 않다. 하지만, 어쨌든 간에 그 라이브러리가 머신에 깔려만 있다면, 헤더파일만 있으면 C에서는 가져다 쓰는 데 큰 문제가 없다.
... 두 번째 이상의 이유를 찾아보려 했지만 도저히 모르겠다. 나도 C로 두 개의 CGI를 '이시대에' 만들었지만, 왜 했는지 지금 생각해보면 참 바보같다. 물론, 이 선택을 하지 않으면 관리자 권한이 없는 웹서버에 내가 원하는 기능을 갖는 CGI 만들기는 불가능했을 것이기 때문에-그리고 그 웹서버를 버릴 수도 없는 상황이라- 어쩔 수 없었지만.
서론은 여기까지 하고, 먼저 단순한 예제를 보자. 아마 다른 언어로 CGI를 만들어 봤으면 다음 예제가 뭐하는 것인지, 더해서 얼마나 무의미한 것인지 알 수 있으리라고 생각한다.
별 것 없다. 컴파일해서 대충 first.cgi 정도로 이름을 바꿔 적당한 곳에 넣어주자. 아, 이야기하는 것을 깜빡했는데, 적어도 이 글은 'C 언어로 된 간단한 소스파일을 이해할 수 있고 CGI 사용을 위한 웹서버 설정을 할 수 있는 사람', 또는 'C 언어로 된 간단한 소스파일을 이해할 수 있고 CGI 사용이 가능하도록 설정된 웹서버를 가진 사람'을 대상으로 하고 있다. 따라서 독자 여러분께서는 first.cgi로 접속하는 법을 알고 있을 것이다.
접속해 보면 웹브라우저에 달랑 ”안녕, 세상아!” 라는 글이(당연히 따옴표는 없다) 찍힌 것을 볼 수 있을 것이다. 이 단순한 소스 코드가 의미하는 바는 다음과 같다.
puts("Content-type: text/plain\n");
실제로는 puts가 문자열을 출력할 때 맨 뒤에 개행문자를 하나 더 붙이므로 개행이 두 번 일어나게 되어 빈 줄 하나가 생긴다. 이 명령이 수행하는 것은 '이 CGI가 출력할 내용물의 타입은 text/plain입니다'하고 클라이언트에게 알리는 것이다. html을 출력할때는 text/html, xml을 출력할 때는 application/xml, png파일일 때는 image/png가 된다. MIME을 찾아봐라.
puts("안녕, 세상아!");
위에서 출력한 헤더의 내용이 나온다. 단순한 텍스트로 안녕, 세상아! 가 출력된다.
기본적으로, CGI가 표준 출력(stdout)으로 출력하는 내용이 클라이언트로 전송되게 된다. 그런데 클라이언트에서 접속했을 때 500에러가 나온다면, 첫째로 헤더 출력하는걸 깜빡했거나 잘못된 헤더를 출력하는 경우이고, Perl이나 다른 언어로 만들었을 경우와 다르게 Segmentation Fault가 난 경우에도 500 에러가 난다. 절대 Segmentation fault 이전까지 출력한 내용이 나오지 않으므로(Apache2의 경우) 알아둘 필요가 있다.
다른 언어로 CGI를 만들어 봤다면 알겠지만, CGI는 서버와 클라이언트의 정보를 환경변수와 표준 입력으로 얻는다. C로 짜는 경우도 큰 차이가 없다. 환경변수 정보를 확인하고 싶다면 다음 CGI를 만들어서 테스트해보면 좋을 것이다.
#include <stdio.h>
int main(int argc, char ** argv, char ** env)
{
int ch;
puts("Content-type: text/plain\n");
while(*env) puts(*(env++));
puts("This is stdin:");
while((ch = fgetc(stdin)) -1) fputc(ch, stdout);
return 0;
}
요정도면 무리 없이 GET/POST 양쪽 모두 사용할 수 있을 것이다. 참고로, %xx 형식으로 escape된 것은 직접 unescape 해 주어야 한다.
그런데, multipart/form-data로 encoding 된 데이터가 넘어올 때는 상당히 곤란하다. 뭔가 많이 복잡해진다는 것을 저 cgi를 통해 확인할 수 있을 것이다. 하지만 파일 업로드를 포기할 수는 없는 노릇. 이럴 때를 위해 누군가가 강력한 라이브러리를 만들어뒀다.
cgilib이라는 라이브러리인데, cgilib 홈 페이지에서 자세한 정보를 접할 수 있다. ...고는 하지만, 뭔가 문서가 부족하다. 하지만 cgilib 자체가 워낙 가볍고 작은 라이브러리인지라, 일반적으로 설치하면 cgi.h 파일이 /usr/include에서 발견되는데 이 파일을 열어서 내용을 보면 금방 이해할 수 있다. cgiInit()로 넘겨진 parameter들을 준비시켜서 cgiGetValue(), cgiGetFile()로 받아오면 된다.
수정사항: CAIRO_PATH를 설정해 주지 않으면 컴파일이 되지 않는데, 까먹고 빼먹었다 -_-
컴파일을 위한 준비물
MinGW, MSYS, GTK Development Environment, 그리고 GNOME2 환경을 구성하는 라이브러리들의dev 버전들.
MinGW는 5.1.6 버전 인스톨러를 받아서 Current 버전들을 설치했다. MSYS는 1.0.11을 다운로드받았다. Gtk+ 2.12.9 Development Environment를 사용하였고, 각각의 라이브러리의 버전은 다음과 같다.
Compile and Install ruby http://ruby-lang.org/ 에서 루비 최신 버전 소스파일을 받는다. 참고로 이번에는 1.8.7 p249를 받아 컴파일했다.
다운로드를 받으면 압축을 푼 디렉토리에서 다음을 수행해서 루비를 컴파일한다.
$ ./configure --prefix=/ruby-gnome2-dev/dist
$ make
$ make install
여기서 --prefix 옵션을 줘서, 모든 작업이 종료된 후 /ruby-gnome2-dev/dist에 ruby와 ruby-gnome2 바인딩을 위치시킬 것이다. 참고로, 여기서 ruby를 다시 컴파일 하는 이유는 루비 홈페이지에서 제공되는 루비는 Microsoft Visual C 컴파일러로 컴파일이 되어 있어서 native extension을 컴파일 할 때 같은 것을 써야만 하기 때문에, ruby-gnome2를 MinGW로 컴파일하기 위해서 ruby를 다시 컴파일하는 것이다.
Compile and Install rcairo
다음 단계는 rcairo를 컴파일하고 설치하는 것이다. http://cairographics.org/releases/에서 rcairo의 최신 버전을 다운받아 압축을 풀고, 그 디렉토리에서 다음을 수행한다.
$ /c/ruby-gnome2-dev/dist/bin/ruby.exe extconf.rb --ruby=/c/ruby-gnome2-dev/dist/bin/ruby.exe --with-override-variables=prefix=/c/GTK
$ make
$ make install
당연한 이야기이지만, 첫 번째 줄에서 줄 바꿈은 없다. 여기서 가정하고 있는 것은 C 드라이버에서 작업중이고 GTK 런타임은 C:\GTK에 설치되었다는 것이다. --with-override-variables=prefix=/c/GTK가 빠지면 이후 pkg-config가 오작동하게 되기 때문에 꼭 필요하다. 위에서는 prefix에 /c/가 없지만 여기서는 필요하다는 것에 주의할 필요가 있다.
Modify Ruby-GNOME2 source code http://ruby-gnome2.sourceforge.jp 에서 최신의 ruby-gnome2 소스파일을 다운받는다. 여기서는 0.19.4를 이용했다.
컴파일 에러를 방지하기 위한 수정이 두 군데 필요하고, 기능상 미비한 점을 보강하기 위한 수정이 두 군데 필요하다. 두 번째 것은 굳이 할 필요는 없지만, 해 두면 여러모로 유익하기 때문에 추가해 둔다. 참고로 참조한 문서에서 수정해야한다고 한 부분은 최신 버전에서는 모두 해결된 것으로 보인다.
이것은 extconf를 수행할 때 뜨는 것으로, 컴파일이 완료됐을 때는 Ignored libraries는 나오지 않는다.
Distribute
루비와 루비 바인딩 자체는 C:\ruby-gnome2-dev\dist 안에 있는 것을 그대로 배포하면 된다. 만약 기존에 설치된 루비에 라이브러리를 추가할 경우는 C:\ruby-gnome2-dev\dist\lib\ruby\site_ruby\1.8 안에 있는 모든 파일을 복사하면 된다.
단순하게 Gtk만 사용할 경우에는 런타임을 다 설치할 것 없이, C:\GTK\bin에 들어 있는 dll들과 config로 끝나는 파일들, reconfig.bat, gdk-pixbuf-query-loaders.exe. gtk-query-immodules-2.0.exe, pango-querymodules.exe 파일을 bin 디렉토리에 복사하고, C:\GTK\lib 안의 gtk-2.0, pango 디렉토리를 lib에, C:\GTK\etc의 fonts, gtk-2.0, pango를 etc에, C:\GTK\share에 sgml, themes, xml을 복사하면 정상적으로 동작한다. 이에 관한 내용은 사실, 좀 더 찾아보고 시도한 다음에 확실하게 할 필요가 있을 듯 하다.
개인적인 프로그램 제작에 있어서 multi-platform 동영상 재생 stub을 만들어야 할 일이 생겼다.
언어는 ruby를 사용해서 어느 정도 cross platform을 구현할 것이고, UI는 맥에도 리눅스에도 심지어 윈도에도 포팅된 GTK를 쓰면 요 세 군데는 무난하게 cross platform 어플이 만들어 지기 때문에 큰 걱정을 안했다.
근데 문제는 동영상 재생이었다. Ruby-GNOME2에 포함된 Ruby/GStreamer는 기본적으로 윈도용에는 컴파일이 되지 않고, 설혹 구현된다 하더라도 윈도에서는 GStreamer 런타임이 '보통' 없는데다 쓰는 어플도 없으니 쓸데없이 파일만 커진다. 따라서, DirectShow로 구현한 동영상 재생 stub을 루비로 가져와서 쓴다면 플랫폼에 따라 GStreamer와 취사선택해서 쓸 수 있을테니 좋을 거라는 생각에 C extension을 짜봤다.
사용하기 전에 DSPlayer.init를 통해 CoInitialize를 호출해 주어야 하고, 다 쓰고 나면 DSPlayer.cleanup으로 CoUninitialize를 호출해 주는 것이 좋다. 참고로 GC에서 제거될 때 생성한 COM object들을 해제해주어야 하지만 구체적으로 손대기 귀찮았기 때문에 각 객체를 close 메서드로 필히 닫아 주어야 한다.
DSPlayer.new로 객체를 생성하면 DSPlayer.open으로 동영상을 열 수 있다. 여기서 주의할 점은, GTK와의 integration을 목적으로 짠 녀석이라, 패러미터로 넘어가는 파일은 utf-8로 인코딩 되어 있어야 한다.
파일을 열었으면 play, pause, stop으로 제어가 가능하고, position 속성을 조작하여 초 단위로(Float) seeking이 가능하다. duration 속성도 사용 가능하다. 마찬가지로 단위는 초다.
snapshot 함수를 사용하게 되면 현재 스냅샷을 bmp 포맷으로 찍어준다. 스트링으로 반환하는데 파일로 저장하면 bmp 파일이 된다.
다른 창에 embeding하기 위해서는 owner_id에 대상 창의 hwnd를 int 형으로 변환해서 넣어주고 재생하면 된다. left, top, width, height 속성으로 동영상 창을 조절할 수 있다.
컴파일 환경은 ruby 1.8.7 (2008-05-31 patchlevel 0) [i386-mswin32]에 Microsoft Platform SDK February 2003, Microsoft DirectX 9.0 SDK Update (Summer 2004), Microsoft Visual C++ 6.0 에서 컴파일했다. 사실 ruby가 MSVC6.0이라 더 최신의 SDK에서 컴파일이 안되는 바람에 저 구시대의 유물들을 발굴하느라 고생 좀 했다 -_-...
네이트온은 쓰다 보면, 다른 데서 로그인하거나 해서 튕긴 경우엔 다시 로그인하기가 귀찮다. 저장된 아이디와 비밀번호를 다 날려버리기 때문이다.
피씨방 등에서 접속하는 일이 많은 일반적인 사람들 기준에서는 저렇게 날려주는게 보안상 이로울 것이다. 아무 생각도 없이 공용 피씨방에서도 자동로그인 옵션을 넣어서 로그인했다가 피싱에 이용당한다던가 하는 일이 많은, 보안 개념이라곤 개미똥꾸멍만큼도 없는 한국인에게 있어서는 정말 유용하다. 유용하긴 하다. 근데 그런 기능은 선택할 수 있게 해 주면 좋지 않을까. 나처럼 두 컴퓨터를 쓰는 경우에는 번갈아가며 로그인해야하는데 저장된 비번이 날아가대니 얼마나 불편한지 모른다. MSN을 좀 닮아주면 안될까.
푸념은 여기까지 하고, 목마른 자가 우물을 판다고 직접 프로그램을 짜 보았다. Win32 API를 써야 할테니 C나 C++을 쓰는 편이 낫겠지만, 연습도 해보고 그냥 끄적거리기엔 루비가 훨 편하니까 루비를 사용해 보았다. 사용한 것은 루비 1.8.7 patchlevel 0 i386-mswin32 버전과 win32api 젬을 사용했다.