Category Archives: 프로그래밍

trac 업그레이드 & plugin

trac 사용하면서 꼭 알아야할 사이트인 trac-hacks.org 에 대해서는 오래전부터 알고 있었지만 크게 필요가 없어서 플러그인은 사용하지 않았었습니다. 티켓에서 예상완료일자를 설정하려고 하니 달력에서 보고 설정할수 있으면 좋겠다는 생각이 들었습니다. 그래서 trac-hacks를 찾아보니 역시 플러그인 있네요 ^^. 물론 trac 자체에서도 custom field를 지원하지만, 달력에서 날짜를 고를수 있는 플러그인인 TracDatePlugin은 꼭 설치하고 싶더군요. 그런데 플러그인을 설치하려고 보니 0.9.X에서는 동작을 안한다고 하더군요. 그래서 업그레이드를 했습니다. 물론 일단 백업받고 시작했죠.

전에 사용하던 버전이 0.9.6이 였는데 FreeBSD ports를 통해서 0.10.4 버전으로 업그레이드했습니다.

# cd /usr/ports/www/trac
# make deinstall
# make install
# trac-admin /TRAC/PATH upgrade
# /usr/local/etc/rc.d/lighttpd restart

trac-admin 실행해주고 웹서버 재시작하니 업그레이드가 정상적으로 완료되었습니다.

trac plugin은 egg라고 zip 파일 형태의 패키지를 이용하도록 되어있습니다. python은 오래전부터 사용했지만, egg 패키지는 처음 사용해보는데 java의 jar 정도 되는거 같네요. 패키지의 종속성등의 메타 정보를 넣을수 있다고 합니다. 하지만, 소스에서 뭘 바꾸려해도 zip 안에 들어있어서 바로 편집하기 어려울듯하네요. Egg에 대한 자세한 내용은 여기를 참고하세요.

egg 파일은 단순히 py, pyc 파일등과 메타 정보를 압축한 zip 파일입니다. egg 파일을 만들기 위해서는 setuptools가 필요하다고 합니다. 역시 FreeBSD ports를 이용해 설치했습니다.

# cd /usr/ports/devel/py-setuptools
# make install

그리고 trac plugin 설명 페이지에서 설명한 대로 ez_setup을 설치했습니다.

# wget http://peak.telecommunity.com/dist/ez_setup.py
# python ez_setup.py

이렇게 하고 필요한 플러그인을 받고 egg 파일을 아래와 같이 만들면 됩니다.

$ unzip datefieldplugin.zip
$ cd datefieldplugin/0.10
$ python setup.py bdist_egg
$ cd dist
$ cp TracDateField-1.0.1-py2.4.egg /TRAC/PATH/plugins

플러그인 소스 받은후 압축풀고, setup.py를 찾아서 위처럼 실행해주면 dist 디렉토리 밑에 egg 파일이 만들어지고, 이 파일을 trac plugin 디렉토리에 옮겨주고 웹서버를 재시작하면 됩니다.

플러그인 중에 제가 사용중인 것 2개만 소개합니다.

TracWebAdmin 0.1.2dev-r4240 : trac-admin을 웹페이지를 통해서 할수 있도록 해주는 플러그인으로 trac을 개발한 edgewall에서 개발한 거고, trac 0.11에서부터는 기본으로 포함된다고 합니다. trac-admin 사용하면 이 정도 기능은 웹에서 가능해야한다고 생각했었는데, 꼭 설치해야할 필수 플러그인이네요.

TracDateField 1.0.1 : custom field에 날짜를 설정할수 있게 해줍니다. 아래와 같이 티켓 만들때 custom field로 date로 설정하면 달력을 보고 날짜를 설정할수 있습니다.


CLucene CJK 분석기 (win32)

전에 CLucene CJK 분석기 리눅스용 소스를 공개하고 윈도우용도 공개하기로 했었는데, 계속 작업 안하고 있다가 마침 요청이 들어와서 정리했습니다.

압축파일에는 clucene-core-0.9.16a가 포함되어 있지 않으며, 압축을 풀고 아래와 같은 폴더 구조로 만들어주시면 됩니다. clucene 소스는 직접 받으셔야 합니다.


CLuceneTest 폴더에 프로젝트(dsw) 파일이 포함되어 있으며, Release에 실행파일을 포함했습니다.

실행을 하면 아래와 같이 간단한 창이 뜨며, 위쪽 텍스트를 라인별로 인덱싱하며, 가운데 검색창에서 쿼리를 입력하고 검색하면 아래 검색 결과가 나타납니다. 검색 버튼을 누를때 위쪽 텍스트에 변경 사항이 있으면 인덱싱을 한후 검색이 됩니다.

윈도우즈에서 CLucene 예제를 만들면서 몇가지 문제점이 있었습니다.

Visual Studio에서 위저드 통해서 MFC 프로젝트 만들면 Preprocessor에서  _MBCS가 정의되어 있는데, 이게 있으면 CLucene이 잘 컴파일이 안됩니다. 두가지 해결방법을 찾긴 했는데 둘다 근본적인 해결책은 아닌거 같네요.

첫번째 방법은 프로젝트 전체에서 _MBCS 정의를 빼버리는 방법이고, 두번째 방법은 CLucene의 “StdHeader.h” 에서 #undef _MBCS 하는 방법입니다. 두번째 방법은 clucene 소스를 한줄 고쳐야하기 때문에 제가 올리는 샘플 프로젝트에서는 첫번째 방법을 사용했습니다.

컴파일 할때 주의 사항은 CLucene에 해당하는 C++ 소스는 “CLucene/StdHeader.h” 파일을 precompiled header로 사용하도록 해야합니다. UI 부분과 Lucene 부분을 분리하여서 UI 쪽에서 Lucene 함수에 직접 접근하지 않도록 했습니다.

컴파일중에 다음 경고가 뜨긴 하는데 실험해본결과 퍼포먼스에 큰 문제는 없었습니다. (제가 사용한 응용 프로그램 기준)

==================Hashing not available or is disabled! CLucene may run slower than optimal ==================

그리고 디버그로 컴파일할때 internal compiler 오류가 납니다.

Fatal error C1076: compiler limit : internal heap limit reached; use /Zm to specify a higher limit

아래처럼 프로젝트 C++ 옵션에서 맨 아래 수동으로 /Zm400 옵션을 추가해주면 문제가 해결됩니다.

소스입니다.

1302547880.zip

GNU autoconf, automake

오픈소스 프로그램들은 대부분 리눅스에서 아래처럼 컴파일할수 있습니다.

$ ./configure
$ make
$ sudo make install

configure 스크립트를 실행하여 시스템에 맞게 컴파일 되도록 Makefile을 생성해줍니다. 유닉스가 종류가 많고 다양하기 때문에 그 모두를 Makefile 하나로만 지원하기는 어렵습니다. autoconf와 automake를 이용하면, 자동으로 타켓 시스템을 분석해주고, Makefile을 자동으로 만들어줍니다. configure를 실행하면 Makefile과 config.h 파일이 생성되는데, config.h에 시스템에서 제공되는 기능에 대해서 매크로로 정의를 해줍니다. 소스 파일에서 config.h를 include하고 매크로에 따라 특정 기능이 지원 안되면 다른 방법으로 구현하여 프로그램의 이식성을 높일수 있습니다. autoconf와 automake를 쓰는것만으로 프로그램 자체의 이식성이 높아지지는 않습니다.

저는 주로 FreeBSD에서 ports를 사용하고, 개발도 Berkeley Parallel make를 이용하기 때문에 autoconf와 automake를 배울 필요를 못느꼈었습니다. 사실 cross platform이 중요하지 않고 소스의 구조가 복잡하지 않으면 make의 기본 기능으로도 큰 불편없이 개발할수 있습니다. 요즘 빌드 시간도 짧으니 dependency도 크게 신경안쓰고 항상 make clean;make 해도 개발 자체가 크게 느려지지는 않더군요. 제가 유닉스에서 개발하는 프로그램은 대부분 5000 라인 이하의 데몬 프로그램들입니다. 거의 C에 가까운 C++ 프로그램이죠.

요즘 AMD64 서버들에 리눅스를 깔고 서버 프로그램들을 리눅스와 프비에서 동시에 지원하기 위해서 작업을 하고 있습니다. 처음에는 Makefile.linux를 따로 만들었는데 Makefile을 따로 관리하기 보다는 autoconf와 automake를 이용하면 더 편할것 같아서, autoconf, automake를 적용해보고 있습니다.

configure 스크립트가 만들어지기 까지 여러가지 명령을 순서대로 실행해야 해서 처음에는 복잡하게 느껴지지만 조금 익숙해지면 어렵지 않습니다. 사용자가 직접 만들어줘야하는 파일은 Makefile.amconfigure.ac 두개입니다. 나머지는 부수적인 파일들로 autoconf, automake 동작에 큰 영향을 주지 않습니다. (AUTHORS, README, COPYING 등)

libevent를 이용한 echo server를 C++로 예제 프로젝트로 만들어봤습니다.

Makefile.am에서는 실행파일과 실행파일을 만들기 위한 소스를 지정해줍니다. 아주 간단합니다.

bin_PROGRAMS = echo_server
echo_server_SOURCES = echo_server.cpp

configure.ac는 조금 복잡합니다. 프로그램 이름, 버전, 이메일 등을 적어주고 사용하는 라이브러리에 대해서 기술하도록 되어있습니다. libevent에 관련한 내용(파란색)이 대부분인데 이 부분은 memcached 에 포함된 configure.ac 파일을 참고했습니다.

AC_INIT(echo_server_libevent, 1.0, mix1009@gmail.com)
AM_INIT_AUTOMAKE(AC_PACKAGE_NAME, AC_PACKAGE_VERSION)
AM_CONFIG_HEADER(config.h)

AC_PROG_CXX
AC_PROG_INSTALL

AC_ARG_WITH(libevent,
   AC_HELP_STRING([–with-libevent=DIRECTORY],[base directory for libevent]))
if test “$with_libevent” != “no”; then
   CFLAGS=”$CFLAGS -I$with_libevent/include”
   CXXFLAGS=”$CXXFLAGS -I$with_libevent/include”
   LDFLAGS=”$LDFLAGS -L$with_libevent/lib”
fi

LIBEVENT_URL=http://www.monkey.org/~provos/libevent/
AC_CHECK_LIB(event, event_set, ,
   [AC_MSG_ERROR(libevent is required.  You can get it from $LIBEVENT_URL)])

AC_CONFIG_FILES(Makefile)
AC_OUTPUT

이 외에도 부수적으로 준비해야하는 파일들이 몇개있습니다. NEWS, README, AUTHORS, ChangeLog는 필수적으로 있어야하는 파일들이고, 몇가지는 automake를 실행할때 –add-missing을 인자로 주면 자동으로 만들어줍니다.

제가 사용하는 FreeBSD 6.2 기준으로 설명드리겠습니다. autoconf, automake 여러버전이 설치될수 있는데, 제가 사용한 버전은 아래와 같습니다.

/home/mix1009> pkg_info | grep auto
autoconf-2.59_2     Automatically configure source code on many Un*x platforms
automake-1.9.6      GNU Standards-compliant Makefile generator (1.9)

아래와 같이 실행하면 configure가 생성됩니다.

/home/mix1009/work/echo_server_libevent> ls -l
total 8
-rw-r–r–  1 mix1009  wheel    66 May  8 13:39 Makefile.am
-rw-r–r–  1 mix1009  wheel   650 May  8 13:39 configure.ac
-rw-r–r–  1 mix1009  wheel  3027 May  8 13:39 echo_server.cpp
/home/mix1009/work/echo_server_libevent> aclocal19
/home/mix1009/work/echo_server_libevent> autoheader259
/home/mix1009/work/echo_server_libevent> autoconf259
/home/mix1009/work/echo_server_libevent> touch README AUTHORS NEWS ChangeLog
/home/mix1009/work/echo_server_libevent> automake19 –add-missing
configure.ac: installing `./install-sh’
configure.ac: installing `./missing’
Makefile.am: installing `./INSTALL’
Makefile.am: installing `./COPYING’
Makefile.am: installing `./depcomp’
/home/mix1009/work/echo_server_libevent> ls -l
total 210
-rw-r–r–  1 mix1009  wheel       0 May  8 13:40 AUTHORS
lrwxr-xr-x  1 mix1009  wheel      35 May  8 13:41 COPYING@ -> /usr/local/share/automake19/COPYING
-rw-r–r–  1 mix1009  wheel       0 May  8 13:40 ChangeLog
lrwxr-xr-x  1 mix1009  wheel      35 May  8 13:41 INSTALL@ -> /usr/local/share/automake19/INSTALL
-rw-r–r–  1 mix1009  wheel      66 May  8 13:39 Makefile.am
-rw-r–r–  1 mix1009  wheel   17475 May  8 13:41 Makefile.in
-rw-r–r–  1 mix1009  wheel       0 May  8 13:40 NEWS
-rw-r–r–  1 mix1009  wheel       0 May  8 13:40 README
-rw-r–r–  1 mix1009  wheel   31538 May  8 13:40 aclocal.m4
drwxr-xr-x  2 mix1009  wheel     512 May  8 13:41 autom4te.cache/
-rw-r–r–  1 mix1009  wheel     640 May  8 13:40 config.h.in
-rwxr-xr-x  1 mix1009  wheel  150289 May  8 13:40 configure*
-rw-r–r–  1 mix1009  wheel     650 May  8 13:39 configure.ac
lrwxr-xr-x  1 mix1009  wheel      35 May  8 13:41 depcomp@ -> /usr/local/share/automake19/depcomp
-rw-r–r–  1 mix1009  wheel    3027 May  8 13:39 echo_server.cpp
lrwxr-xr-x  1 mix1009  wheel      38 May  8 13:41 install-sh@ -> /usr/local/share/automake19/install-sh
lrwxr-xr-x  1 mix1009  wheel      35 May  8 13:41 missing@ -> /usr/local/share/automake19/missing

처음 없는 파일들을 만들기 위한 명령인 touch와 automake 실행할때 –add-missing은 파일이 만들어진 이후로는 인자를 주지 않아도 됩니다. 파일이 모두 만들어진 이후로는 아래와같은 명령만 쳐주면 됩니다.

/home/mix1009/work/echo_server_libevent> aclocal19
/home/mix1009/work/echo_server_libevent> autoheader259
/home/mix1009/work/echo_server_libevent> autoconf259
/home/mix1009/work/echo_server_libevent> automake19


그리고 –add-missing으로 파일을 생성할때 심볼릭 링크로 생성되는데 소스를 배포하거나 CVS에 commit하기 전에, 파일을 직접 복사하는게 좋습니다.

configure 스크립트가 생성되면, 아래와 같이 타켓 시스템에서 컴파일할수 있습니다. configure가 만들어지기 전까지는 하나의 서버에서 실행해서 결과 파일들(configure, Makefile.in, config.h.in 등)을 배포하면 되지만, 이후 부분부터는 타켓 플렛폼에서 실행해야 합니다. configure가 실행될때 타켓 시스템에 특정 기능이 구현되었는지를 검사하고 라이브러리 설치여부등을 검사합니다. 그 결과에 따라서 config.h 파일을 만들어줍니다.

아래처럼 타겟 시스템에서 컴파일하고 설치할수 있습니다.

/home/mix1009/work/echo_server_libevent> ./configure –with-libevent=/usr/local
checking for a BSD-compatible install… /usr/bin/install -c
checking whether build environment is sane… yes
(생략)
checking for event_set in -levent… yes
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands
/home/mix1009/work/echo_server_libevent> make
(생략)
/home/mix1009/work/echo_server_libevent> sudo make install
(생략)

리눅스에서는 configure를 인자없이 실행해도 libevent를 정상적으로 찾는데, FreeBSD에서는 위와 같이 인자로 지정해주어야 찾습니다.

각 툴들을 간략히 설명하겠습니다. aclocal은 autoconf에서 미처 구현되지 못한 부분(?)을 처리하는 것으로 차후에는autoconf에 포함될거라고 합니다. autoheaders는 configure.ac로부터 config.h.in을 만듭니다. automake는 configure.ac와 Makefile.am 내용에 따라서 Makefile.in을 생성합니다.autoconf는 configure.ac에 따라서 configure를 생성합니다. configure는 config.h.in과Makefile.in을 기준으로 시스템 정보를 분석하여 Makefile과 config.h를 만들어줍니다.

각 프로그램의 입출력 파일들을 살펴보면 아래와 같습니다. http://sourceware.org/autobook/의 Appendix C를 참고했습니다.

configure.ac — aclocal —> aclocal.m4
configure.ac, (optional aclocal.m4) — autoheaders —> config.h.in
configure.ac, Makefile.am — automake —> Makefile.in
configure.ac, (optional aclocal.m4) — autoconf —> configure
Makefile.in, config.h.in — configure —> Makefile, config.h

처음에는 복잡하게 느껴지지만, 한번 사용해보면 Makefile.am에서 소스 파일만 추가하고, 사용하는 라이브러리가 추가될때 configure.ac에서 라이브러리 지정만 하면 되기 때문에 사용에 어려움이 없습니다. 도입후에 여러 플렛폼에서 컴파일하면서 발생하는 문제점들을 하나씩 해결해 나가면 프로그램의 이식성을 높일수 있습니다. 문제가 발생하는 함수등을 configure.ac에서 지정하고 config.h에 정의된 매크로에 따라서 소스에서 적절히 처리하면 이식성이 차차 높아지게 됩니다.

autoconf, automake 실행하기 전 소스입니다. libevent를 이용한 echo 서버 예제 입니다. 소스 자체의 완성도는 높지 않습니다. configure.ac, Makefile.am, echo_server.cpp, COPYING 네개의 파일이 포함되어있습니다.

1214426580.tgz

ocaml-event를 이용한 echo server

ocaml-event(libevent ocaml wrapper)를 이용하여 echo server를 OCaml 언어로 작성해 봤습니다. libevent C API에 익숙해서 ocaml 답지 않게 짜진거 같은 느낌도 들지만 워낙 프로그램이 간단하다보니 크게 틀려질 부분은 없어보이는군요. 다른건 문제가 없었는데 Makefile에서 ocamlopt로 native 컴파일할때 C 라이브러리가 링크가 안되서 약간 헤맸고, printf 후에 flush stdout을 하는 부분이 있는데, stdout이 Unix 네임스페이스에 있는 stdout으로 인식이 되어 컴파일이 안되더군요. 그래서 Pervasives.stdout으로 지정하니 컴파일 잘되네요.

아래는 echo_server_event.ml 소스입니다.

[CODE type=ocaml]
(*
* echo server using libevent
*
* Copyright (c) 2007 Chun-Koo Park
* All rights reserved.
*)

open Unix

let rec sock_write sock buf offset = function
  | 0 -> ()
  | len ->
     let nwritten = write sock buf offset len in
       sock_write sock buf (offset + nwritten) (len – nwritten)

let echo_callback event fd event_type =
  let buf = String.create 64 and nread = ref 1 in
   while !nread > 0 do
     nread := read fd buf 0 64;
     sock_write fd buf 0 !nread;
   done;
   if !nread > 0 then
     Libevent.add event None
   else
     Printf.printf “connection closed\n”;
     flush Pervasives.stdout

let accept_callback event fd event_type =
  let asock, addr = accept fd in
  let evnew = Libevent.create () in

  Libevent.set evnew asock [Libevent.READ] false (echo_callback evnew);
  Libevent.add evnew None;

  Libevent.add event None

let tcp_server_sock port =
  let ssock = socket PF_INET SOCK_STREAM 0
  and addr = inet_addr_any in
   bind ssock (ADDR_INET (addr, port));
   setsockopt ssock SO_REUSEADDR true;
   listen ssock 5;
   ssock

let _ =
  let listenport = 2007 in
  let acceptfd = tcp_server_sock listenport in
  let acceptev = Libevent.create () in

  Libevent.set acceptev acceptfd [Libevent.READ] false (accept_callback acceptev);
  Libevent.add acceptev None;

  Libevent.dispatch ()
[/HTML][/CODE]

ocaml-event를 이용한 echo 서버와 thread와 fork로 짠 echo 서버의 Makefile입니다. ocaml findlib를 이용했습니다. ocaml findlib에 대해서 얼마전에 올린글을 참고하세요.

[CODE type=makefile]
# Makefile
#
# Copyright (c) 2007 Chun-Koo Park
# All rights reserved.

OCAMLC=ocamlfind ocamlc
#OCAMLC=ocamlfind ocamlopt

EXEC=echo_server_fork echo_server_thread echo_server_event
all: $(EXEC)

echo_server_fork: echo_server_fork.ml
   $(OCAMLC) -o $@ -package unix -linkpkg $@.ml

echo_server_thread: echo_server_thread.ml
   $(OCAMLC) -o $@ -thread -package “threads unix” -linkpkg $@.ml

echo_server_event: echo_server_event.ml
   $(OCAMLC) -o $@ -package “libevent unix” -linkpkg $@.ml -ccopt -L/usr/local/lib -cclib -levent

clean:
   rm -f *.cmo *.cmi *.cmx *.o $(EXEC)

# vi:set noet:
[/HTML][/CODE]

thread 사용할때는 -thread 옵션을 주고, ocamlopt로 native 컴파일할 경우를 위해서 -cclib로 libevent C 라이브러리를 지정해주었습니다. -cclib event로 지정하면 라이브러리를 찾을거라 생각했는데 -cclib -levent 형태로 라이브러리 지정을 해야되더군요. ocamlc로 byte compile할때는 -cclib 를 지정하지 않아도 됩니다.

Ocaml로 작성한 세가지 echo server 소스입니다. ( libevent / fork / thread )

1402904692.tgz

ocaml findlib : ocamlfind

OCaml 언어에 예전부터 관심이 많았었는데, 오랜만에 다시 보니 새롭군요. 그전에 배우면서 CVS에 올렸던 날짜를 보니 2002년이네요. 5년이 지났지만 실전에 아직 OCaml를 쓸 기회가 없다보니 조금 안쓰다보면 까먹어서 자꾸 다시 배우게 되네요.

그 전에 배울때는 OCaml에 포함된 기본 컴파일러만 make와 병행해서 사용했었는데, ocaml-event 라이브러리를 깔다보니 ocamlfind라는 실행파일을 통해서 인스톨하게 되어 있더군요. ocaml-event는 libevent의 ocaml에서 사용할수 있도록 하는 라이브러리입니다. FreeBSD ports중에 /usr/ports/devel/ocaml-findlib를 깔고, ocaml-event는 수동으로 소스를 컴파일하여 인스톨했습니다. /usr/ports/devel/ocaml-event 에 있지만 여기서 인스톨하면 ocamlfind에 등록이 이상하게 안되더군요…

ocaml-findlib를 깔면 중심 실행파일이 ocamlfind인데, 이 파일이 상당히 다양한 기능들은 가지고 있습니다. Ocaml 라이브러리/프로그램 인스톨, 언인스톨, 컴파일, 소스에서 문서 추출, 라이브러리 종속성 조회, 라이브러리 브라우징 등의 기능을 제공하며, 브라우징의 경우 Tk를 이용하여 X11 환경에서만 실행됩니다. 자세한 것은 man 페이지나 findlib user guide 문서를 참고하세요.

저는 컴파일 할때 ocamlc나 ocamlopt를 바로 호출하지 않고 ocamlfind를 통해서 호출한 방법만 설명드리도록 하겠습니다. ocamlc는 자바처럼 byte code 형태로 컴파일되고, ocamlopt는 타겟 CPU에서만 실행되는 native 형태로 컴파일이 됩니다. 한 ocaml 프로그램 내에서 byte code와 native code로 컴파일된것을 링크할수 없으므로 Makefile를 이용한다면 이를 따로따로 관리해야합니다. ocamlfind를 이용하면 cmo(byte code object)나 cmx(native object)를 지정하지 않아도 되서 Makefile의 크기가 많이 줄어드는것 같네요. 라이브러리 지정도 cma, cmxa를 따로 지정하지 않으며, 또한 라이브러리의 종속성을 자동으로 판단하여 라이브러리 링크 순서도 자동으로 판단해주는거 같습니다.

예전에 짰던 echo_server fork버전과 thread버전의 Makefile.old와 변경된 Makefile.new을 비교해보시면 ocamlfind가 유용하다는걸 느끼실수 있을겁니다. 그리고 Makefile.old는 native로 컴파일할려면 많은 부분 고쳐야하지만, Makefile.new에서는 한줄만 바꾸면 됩니다.

[CODE type=make]
# Makefile.old
# Copyright (c) 2002 Chun-Koo Park

OCAMLC=ocamlc
OCAMLCFLAGS=-thread
.SUFFIXES: .ml .mli .cmo .cmi

LIBS=unix.cma

EXEC=echo_server_fork echo_server_thread
all: $(EXEC)

clean:
   rm -f *.cmo *.cmi *.cmx *.o $(EXEC) .depend

.mli.cmi:
   $(OCAMLC) $(OCAMLCFLAGS) -c $<

.ml.cmo:
   $(OCAMLC) $(OCAMLCFLAGS) -c $<

echo_server_fork: $@.cmo $(OBJS)
   $(OCAMLC) -o $@ $(OCAMLCFLAGS) $(LIBS) $@.cmo

echo_server_thread: $@.cmo $(OBJS)
   $(OCAMLC) -thread -o $@ $(OCAMLCFLAGS) unix.cma threads.cma $@.cmo

# vi:set noet:
[/HTML][/CODE]

[CODE type=make]
# Makefile.new
# Copyright (c) 2007 Chun-Koo Park

OCAMLC=ocamlfind ocamlc
#OCAMLC=ocamlfind ocamlopt

EXEC=echo_server_fork echo_server_thread
all: $(EXEC)

echo_server_fork: echo_server_fork.ml
   $(OCAMLC) -o $@ -package unix -linkpkg $<

echo_server_thread: echo_server_thread.ml
   $(OCAMLC) -o $@ -thread -package “threads unix” -linkpkg $<

clean:
   rm -f *.cmo *.cmi *.cmx *.o $(EXEC) .depend

# vi:set noet:
[/HTML][/CODE]

위 Makefile 들은 FreeBSD에서 BSD Parallel Make에서 테스트된 파일들입니다. 마지막 줄은 vim에서 탭을 자동으로 공백으로 확장하지 않도록 하는 명령입니다. (noexpandtab)

findlib에 포함된 make_wizard를 사용하면 위저드 형태로 7단계로 Makefile를 만들어줍니다. 생성되는 Makefile이 상당히 길고, 생성된 Makefile은 위저드로 편집이 불가능하기 때문에 사용이 꺼려지긴 합니다. 하지만, 참고할만한 마땅한 Makefile이 별로 없었는데 소스만 봐도 큰 도움이 될듯하네요. 다음은 make_wizard를 실행한 화면입니다.

UNIX Socket Model & libevent

회사에서 libevent에 대한 스터디를 하기로 했습니다. 서버 프로그램 개발시 FreeBSD에서 KQueue와 Solaris에서 /dev/poll 개발 경험이 있습니다. 최근에는 리눅스 서버에서 libevent를 이용하여 서버 프로그램도 제작했습니다. OS별로 각기 API가 조금씩 틀려서 이식성이 떨어져서 이제는 libevent로 통일해서 개발하려고 합니다.

여기서 설명하는 소켓 모델은 여러 소켓을 한 프로세스에서 동시에 처리하기 위한 방식으로, 주로 단일 쓰레드 방식의 어플리케이션에서 여러 소켓 연결(세션)을 처리하기 위해서 사용합니다. 소켓 뿐 아니라 파일 IO 등을 같이 처리할수 있으며, File Descriptor를 통해서 액세스 합니다.

select와 poll이 유닉스 표준으로 정해져 있는 함수이며, 프로토타입은 다음과 같습니다.
[CODE type=cpp]
int select(int nfds, fd_set *r_fds, fd_set *w_fds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
[/HTML][/CODE]

select와 poll 모두 file descriptor 리스트를 인자로 넘기고, timeout 동안 기다립니다. Timeout 시간 이전에 전달한 fd 중에 이벤트가 있으면 리턴합니다.

단일 쓰레드 소켓 서버 프로그램의 pseudo code는 대충 아래와 같습니다.
[CODE type=cpp]
while (1) {
  setup_fd_list();
  select_or_poll();
  handle_fd_events();
}
[/HTML][/CODE]

Select는 fd의 리스트를 비트 단위로 인코딩해서 보냅니다. FD_SETSIZE를 정의해서 bitset의 길이를 정해줄수 있지만, 시스템에 따라서 제약이 있는 경우도 있다고 합니다. Poll은 struct array로 fd 리스트를 전달하여 길이에는 제한이 없지만, array의 중간 fd가 지워지면 처리가 좀더 복잡해집니다. Select와 poll 모두 호출할때마다 fd 리스트 전체를 전달하기 때문에 연결수가 많아지면 비효율적이 됩니다. 유저스페이스에서 커널로 전달하는 데이타가 연결수에 비례하여 선형적으로 늘게됩니다.

아래는 libevent 홈페이지에 있는 소켓 모델간의 비교 그래프입니다. select와 poll 모두 연결수가 많아질수록 처리시간이 비례하여 증가하는것을 볼수 있습니다.

이러한 문제점을 해결하기 위해서 OS별로 다른 소켓 모델을 개발했습니다. BSD에서는 Kqueue, Solaris에서는 /dev/poll, Linux에서는 epoll 등이 있습니다. 이 모델들은 fd 리스트 전체를 항상 전달하지 않고, 한번 등록한 fd는 호출할때 다시 등록하지 않아도 등록된 상태로 되어 커널 쪽에서 관리가 됩니다. fd를 등록하고, 나중에 삭제할 때나 이벤트가 있을 때 까지 등록된 상태로 남아있어, 커널과 유저스페이스 간의 통신 오버헤드를 대폭 줄였습니다. 따라서 연결이 늘더라도 이를 커널쪽에서 효율적으로 처리하여 처리시간이 크게 늘지 않도록 구현되어 있습니다. 위에 그래프를 보면 Kqueue와 epoll은 연결수가 늘어나도 처리시간은 거의 일정한 것을 관찰할수 있습니다.

하지만 이러한 소켓 모델들은 표준화 되지 않고, 구현 범위도 조금씩 차이가 나기때문에 이식성이 좋은 소켓 서버를 구현하는 것은 쉽지 않습니다. Libevent는 이러한 소켓 모델들을 모두 지원하는 라이브러리로 시스템에 따라서 제일좋은 소켓 모델을 선택하여 동작하도록 되어 있습니다.

참고 URL: http://www.kegel.com/c10k.html

c10k 문제(10000 연결 문제)라고 하여 연결을 동시에 많이 유지하는 서버 프로그래밍에 대해서잘 정리되어 있는 페이지입니다.

스터디 주제가 이미 어느정도 아는 내용으로 정해졌고, 새로 오신 분들이 유닉스 경험이 많지 않아서 스터디하면서 나름대로 새로운 내용들을 공부해봐야겠습니다. 일단 개인 관심언어인 Ocaml로 ocaml-event를 이용해서 서버 프로그램을 작성해보고, Make대신 automake와 autoconf 공부도 좀 해야겠네요. 워낙 BSD Parallel Make에 익숙해져서 요즘 리눅스에서 GNU Make 쓰는데 당황스럽더군요. –;

CLucene CJK 분석기

CLucene을 이용하여 검색엔진 구현하는데, 한글처리에 대한 정보가 거의 없더군요. 아주 기본적인 한글처리만 구현해보았습니다. CLucene에서 한글 처리에 대해서 참고하시면 도움이 되리라 생각하여 소스를 공개합니다.

리눅스와 윈도우즈에서 동작하지만 먼저 리눅스 소스만 공개합니다. 윈도우즈에서 아직 _MBCS 정의를 빼지않고 컴파일에 성공하지 못했습니다. 좀더 연구해봐야할듯하네요. 소스는 코드변환 외에는 차이가 없습니다.

clucene-core-0.9.16a 버전을 사용했으며 Makefile에서 CLUCENEPATH를 설정하고 make하시면 됩니다. 소스에 포함된 한글은 UTF-8로 인코딩되어있으며, CentOS 4.4 AMD64 리눅스(LANG=ko_KR.UTF-8)에서 테스트했습니다.

clucene의 StandardTokenizer에 보면 CJK관련 처리가 있지만, next()에서 _CJK로 인식하기 전에 다른곳(_istalpha)으로 빠져서 CJK 토큰으로 분류가 안되더군요. 그래서 복사해서 CJKTokenizer.cpp를 만들고 비교 순서만 바꿔줬습니다. 왜 한글코드가 _istalpha으로 인식되어 빠져나가는지는 잘 모르겠네요.

KoreanStemFilter.cpp에서는 CJK 토큰을 2글자 단위로 나누는 역할을 합니다. 루씬인액션에 설명되어있는데 clucene에는 구현이 안되어 있는거 같더군요. “검색엔진” 토큰을 “검색” “색엔” “엔진” 토큰으로 바꾸죠. 한글의 조사를 뺀다던가 하는 기능을 추가하기 위해서 KoreamStemFilter로 만들었는데 지금 기능은 CJK 필터만 구현되어있네요.

ConvertUtil.cpp는 iconv를 이용하여 UTF-8을 UTF-32LE로 바꾸는 소스 입니다. 윈도우즈의 경우는 MultiByteToWideChar()와 WideCharToMultiByte() 함수를 이용했습니다.

CLuceneTest.cpp는 간단한 데이타 3개를 넣고 터미널 상에서 검색할 수 있는 테스트 프로그램입니다. clucene의 데모 소스를 약간 바꿔서 구현했습니다.

다음은 실행한 화면입니다:

$ ./CLuceneTest
adding doc: doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진
adding doc: doc2 – hello zaza 한글 김현정 김건 건모 검색
adding doc: doc3 – goodbye 김건모 서영은 검색 엔진 SG워너비
Indexing took: 5 ms.

Enter query string: 검색엔진
Searching for: “검색 색엔 엔진”

0. doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진 (0.974307)

Search took: 1 ms.
Screen dump took: 0 ms.

Enter query string: +검색 +엔진
Searching for: +검색 +엔진

0. doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진 (0.383675)
1. doc3 – goodbye 김건모 서영은 검색 엔진 SG워너비 (0.383675)

Search took: 0 ms.
Screen dump took: 0 ms.

소스입니다.
1204400252.tgz

검색 기술 좌충우돌

검색 기술은 제대로 배워본적이 없었는데, 서비스 운영하다보니 여러번 발목을 잡더군요. 서버에서 검색 기술이 필요한 부분도 있었고, 클라이언트에서도 필요했습니다.

클라이언트 단에서는 처음에는 인덱스 없이 직접 스트링 비교를 해서 검색하다가, 검색할 데이타가 늘면서 느려져서, 검색 인덱스를 메모리에 간단히 만들고 (stl map 이용), 사용하니 큰 문제없이 잘 동작하더군요. 근데 경우에 따라서 검색 인덱스가 아주 커지는 클라이언트 들이 생겨서 메모리에 전체 인덱스를 올리는것이 불가능한 경우가 생겼습니다. 그래서, 어쩔수 없이 파일 기반으로 map처럼 사용할수 있는 dbm 계열의 라이브러리를 사용하여 인덱스를 저장하여 사용하고 있습니다. 현재까지는 큰 문제없이 사용하고 있습니다. 하지만 검색 인덱스가 커지고 쿼리가 늘면 좀 버벅대더군요. 아무래도 인덱스 만드는거나 검색자체가 효율적으로 구현한것이 아니고, 저장하는 방법 또한 효율적으로 검색할수 있는 구조가 아니었던것 같네요.

서버 단에서는 디비에 있는 필드 검색이라 MySQL의 FullText 검색을 이용할 방법을 고민했었습니다. 요즘도 해결 안됐을거 같은데 FullText 검색은 한글을 지원하지 않죠. 그래서 고민하다가 생각한 방법이 한글을 영문으로 인코딩하여 필드하나 더 만들어서 박아넣는거였습니다. 일단 영문은 다 소문자로 변경해서 넣고, 한글은 대문자로 자~알 인코딩 해서 넣고, 디비 Insert,Update할때 인덱스 필드에 변환해서 넣고, 검색할때 한글 변환해서 fulltext search하고.. 이렇게 하니 잘 동작은 하더군요. 몇달간 서비스하면서 데이타가 몇기가 쌓이니, 점점 느려져만 가더군요. 이건 서비스 기획의 문제였지만, 운영하던 서비스는 거의 삭제의 개념이 없고 계속 누적되는 형태로 운영이 되었습니다. 그러니 문제가 될수 밖에 없었죠. 나중에는 검색을 새로 구현할 엄두가 나지 않아서 검색 자체를 빼고 P2P 검색으로 대체했습니다…

서버 단에서 다시 검색이 필요하여, MySQL fulltext보다는 좀더 좋은 방법을 찾으려고 했는데… 인덱싱하는 페이지들이 모두 html로 만들어져 있기 때문에, 웹기반 인덱싱 쪽으로 알아보았습니다. 외국에서 만든 많은 검색엔진들이 단어 단위로 인덱스를 만들기 때문에 한글 검색이 잘 안되는 경우가 많더군요. “검색엔진”이라는 단어가 인덱싱 되면 “검색”으로는 해당 문서가 검색이 안되는 엔진들이 많더군요. mnogosearch라는 검색엔진을 발견했는데, 한글 검색이 완벽하게 되더군요. 구현할 시간은 없고 일단 동작을 하다보니 그냥 사용하기로 하고 서비스 했습니다 –; 뭐 처음에는 사용자들이 검색 들어갔는지도 잘 몰랐기 때문에, 서비스 운영에 전혀 문제 없었습니다. 데이타가 쌓이고, 검색 많아지고 하면서 문제가 조금씩 생기더군요. 일단 서버 여러대에 분산 처리했습니다. 근데 문제는 몇몇 사용자가 검색을 집중적으로 해도 서비스가 상당히 불안해졌습니다. 서비스가 다운되거나 그런건 아니지만, 디비에 부하가 걸려서 응답속도가 느려지더군요. mnogosearch에 대한 최적화 방법 같은걸 찾아보고, 들어오는 쿼리들을 저장하고 벤치마킹을 해봤습니다. 처음 벤치마크 결과는 절망적이더군요. 일단 쿼리에 따라 검색시간에 큰 편차가 있었습니다. 내부적인 동작을 모르니, 최적화는 한계가 있을거 같다는 생각을 하고 mnogosearch를 대체할수 밖에 없다고 결론을 냈습니다.

로레벨(ㅎㅎ) 개발자이기 때문에 먼저 위키피디아에서 검색 알고리즘들을 찾아보고, 필요한 소스코드(주로C)를 수집하기 시작했습니다. 알고리즘 소스들을 연결하여 인덱싱하고 검색하는거 구현하는 거는 정말 쉬운일이 아닐거 같다는 생각이 금방 들더군요. 그래서 좀더 검색하다 찾은것이 루씬(Lucene)입니다.

루씬은 자바로 된 검색엔진입니다. 검색엔진이라는 용어 자체가 좀 애매한데, mnogosearch는 사용자를 위한 검색 엔진이라면, 루씬은 개발자용 검색 엔진입니다. mnogosearch는 인덱스할 url을 입력하고, 화면에 검색결과가 어떻게 표시될지만 설정해주면 바로 사용이 가능합니다. 루씬의 경우는 검색엔진 라이브러리입니다. 개발자가 직접 문서를 읽어들이는 코드, 인덱싱하고, 검색하는 코드를 직접 작성해야합니다. 또한 어느정도 기술에 대한 이해도가 있어야 좋은 성능을 낼수 있다고 하네요. 다행히 Lucene in Action이라는 책이 나와있어서 큰 도움이 되더군요. 번역판도 나온거 같더군요…

제가 자바를 선호하지 않고, 잘 동작한다면 클라이언트(win32)단에서도 사용하고 싶었기 때문에 C++로 포팅된 CLucene을 사용하기로 결정했습니다. CLucene이 자바 루씬에 비해서 버전이 낮긴하지만, 성능도 뛰어나고, 제가 C++이 더 익숙해서요…. win32 클라이언트는 종속성을 줄이기 위해서 static 링크되지 않는 외부라이브러리는 가능하면 안씁니다. ( 특히 .net 같은거는 절대 안씁니다. )

CLucene으로 현재 검색 서버를 개발 완료했으며, 클라이언트 단에도 실험적으로 돌려보고 있는데 성능은 아주 잘 나오고, 검색 시간 편차는 정말 적더군요. 서버단에서 벤치마크 결과 아주 만족스런 결과가 나왔습니다.

시간내서 CLucene으로 한글 검색 되도록 스탬필터로 개발한 소스를 공개하도록 하겠습니다. 한글스탬필터는 2글자씩 짜르는거 외에 별도 처리는 구현하지 않았습니다. 자바 루씬에는 이렇게 구현된 것이 들어있는지 모르겠는데, CLucene에서는 소스 찾아봐도 없더군요. 그리고 CLucene의 StandardAnalyzer로는 CJK 처리가 정상적으로 동작안하더군요. (win32, linux에서 모두)

소스 조만간 공개하겠습니다 ^^

XP에서 멀티쓰레드 프로그램 디버깅시 멈추는 문제

XP에서 개발한 이후로 디버깅하면서 모든 프로그램이 멈추는 듯한 현상이 자주 발생했었는데, 이제야 문제에 대한 해결책(?)을 우연히 블로그에서 발견하게 됐습니다.

http://snaiper.tistory.com/198

2000에서 개발할때까지는 디버깅에 전혀 문제가 없었는데, 멀티쓰레드 프로그램 디버깅을 할때 메인 쓰레드가 아닌곳에 breakpoint를 잡고 디버깅하다 보면 마우스만 움직이고 전체 프로그램이 클릭도 안되는 상황이 발생하더군요. 그래서 2000에 개발환경도 갖춘적도 있었는데, 최근에는 주로 OutputDebugString로 원시적인 디버깅을 많이합니다. ㅎㅎ

정확한 원인은 잘 모르겠지만, 고급텍스트서비스라는데서 글로벌 락을 이용해서, 디버거에서 그쪽에 락을 걸어버려서 다른 모든 프로그램들도 데드락이 걸리는거라는 거 같습니다. –;

해결책은

제어판-국가및언어옵션-언어탭-텍스트 및 입력언어 “자세히”-고급탭-고급텍스트서비스사용안함에 체크하고 리부팅

이렇게 바꾸면 멈추는 현상이 대부분(?) 없어진다고 하는군요.

윈도우즈에서 OCaml 개발 환경 (tuareg)

예전엔 FreeBSD에서 vi로 작업했었는데, Lisp/Slime 환경에 친숙해지면서 OCaml에서도 비슷한 개발환경을 찾아보게 됐습니다. Emacs외에도 몇가지 통합개발환경이 있지만 저는 Emacs 위에서 돌아가는 tuareg mode를 받아서 설치했습니다. 저는 로컬 윈도우즈에서 Emacs를 사용하지만 유닉스에서도 동일하게 사용할수 있습니다. OCaml은 mingw 기반의 설치파일을 받아서 설치했습니다.

tuareg는 tuareg-mode-1.46.1.zip를 받아서 Emacs의 site-lisp에 압축을 풀었습니다. 그리고는 다음을 .emacs 파일에 추가해주었습니다.

[CODE type=lisp](add-to-list ‘load-path “c:/Progra~1/emacs/site-lisp/tuareg-mode-1.46.1”)
(setq auto-mode-alist (cons ‘(“\\.ml\\w?” . tuareg-mode) auto-mode-alist))
(autoload ‘tuareg-mode “tuareg” “Major mode for editing Caml code” t)
(autoload ‘camldebug “camldebug” “Run the Caml debugger” t)

(defvar tuareg-interactive-program “c:/Progra~1/Object~1/bin/ocaml”)
(defvar tuareg-library-path “c:/Program Files/Objective Caml/lib”)
[/HTML][/CODE]
tuareg-interactive-program 지정할때  Program Files와 Objective Caml등으로 넣으면 OCaml 실행이 안되더군요.. 위 사항을 적용하면 ml이나 mli 파일을 읽을때 tuareg-mode로 자동으로 넘어갑니다.

키는 Lisp/Slime과 많이 유사하지만, Slime보다는 제공하는것이 적네요. C는 Ctrl, M은 Alt(Meta)키 입니다.

  • C-x C-e, C-c C-e, C-M-x : 세가지 키조합 모두 현재 expression을 실행합니다. OCaml이 실행중이 아니면 실행합니다.
  • C-c C-b : 현재 버퍼(파일)을 실행합니다.
  • C-c C-c : make를 실행하는데 프로젝트에 맞는 Makefile이 있어야합니다.
  • C-M-p, C-M-n : (toplevel) expression 위아래로 이동.
  • C-c C-a : ml과 mli 사이를 왔다갔다 합니다. 아주 유용한 기능이네요.

또 Emacs메뉴에 Tuareg에 보면 여러가지 기능이 제공됩니다. Definition-Scan을 선택하면 현재 파일에서 모든 정의(type & value)를 정리해서 메뉴에 보여줍니다. 또 C-c . 으로 시작하는 명령들이 있는데, 많이 사용하는 구절들을 빠르게 입력할수 있게 도와줍니다. 한가지 아쉬운점은 completion 기능이 동작하지 않는데, 왜 그런지 아직 파악하지 못했습니다. 메뉴에 보면 관련 항목이 있는데, 활성화 되어있지 않습니다. 또한 키도 할당되어 있지 않습니다. 소스를 보면 C-c TAB( (define-key map [?\C-c ?\t] ‘tuareg-complete) 로 할당되어 있는데, 이 키는 Interrupt Caml Toplevel로 할당되어 보이고, M-x tuareg-complete로 호출해도 “Symbol’s function definition is void: caml-complete”라는 메시지만 출력됩니다.

좀더 연구 해봐야겠습니다…