ZeroMQ – 1

ZeroMQ 는 네트워크 소켓 관리를 위한 라이브러리이다.

이번 프로젝트를 진행하면서 Libevent + ZeroMQ 를 사용하게 되었는데 너무 효과가 좋아서 그 내용을 정리한다.

이곳에 올려진 모든 ZeroMQ 관련 내용은 모두 zguide.zeromq.org 에 있는 내용을 바탕으로 정리해 놓은 것이며, 모든 원문 내용은 해당 사이트에서 무료로 볼 수 있다.

모든 내용은 C 를 기반으로 작성되며, 다른 언어의 경우, 마찬가지로 해당 사이트에서 확인할 수 있다.

먼저 ZeroMQ를 설치하자.

ZeroMQ 설치는 간단하다. Ubuntu 의 경우, 다음의 명령어를 입력하면 된다.

$ sudo apt-get install libzmq

라이브러리 설치가 완료되었으면 곧바로 예제 프로그램을 작성해보자.

예제 프로그램은 당연히 Hello world 프로그램이다.

두 개의 프로그램을 작성한다.  Server/Client.

server 와 client는 서로 tcp/ip 로 통신하며, server 는 프로그램을 시작하면 client 의 접속을 기다린다. 이후, client 가 동작하면 곧바로 server 로 접속을 시도한다.

client 가 server 접속을 하면, “Hello” 메시지를 송신하며, server 는 메시지 수신 이후, “World”라는 응답을 보낸다.

main.c
#include
#include
#include
#include
#include

int main(int argc, char** argv)
{
// Socket to talk to clients
void* context = zmq_ctx_new();
void* responder = zmq_socket(context, ZMQ_REP);
int rc = zmq_bind(responder, "tcp://*:5555");
assert(rc == 0);

while(1)
{
char buffer[10];
zmq_recv(responder, buffer, 10, 0);
printf("Received Hellon");
sleep(1); // Do some 'work'
zmq_send(responder, "World", 5, 0);
}

return 0;
}

Client 도 작성해 주자.

main.c


// Hello World client
#include
#include
#include
#include

int main(int argc, char **argv)
{
printf("Connecting to hello world server...n");
void* context = zmq_ctx_new();
void* requester = zmq_socket(context, ZMQ_REQ);
zmq_connect(requester, "tcp://localhost:5555");

int request_nbr;
for(request_nbr = 0; request_nbr != 10; request_nbr++)
{
char buffer[10];
printf("Sending Hello %d...n", request_nbr);
zmq_send(requester, "Hello", 5, 0);
zmq_recv(requester, buffer, 10, 0);
printf("Received World %dn", request_nbr);
}

zmq_close(requester);
zmq_ctx_destroy(context);

return 0;
}

자, 이제 서버를 먼저 실행하고, Client 를 실행해보자.

Server 실행 결과

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Received Hello

Client 실행 결과

Connecting to hello world server…

Sending Hello 0…

Received World 0

Sending Hello 1…

Received World 1

Sending Hello 2…

Received World 2

Sending Hello 3…

Received World 3

Sending Hello 4…

Received World 4

Sending Hello 5…

Received World 5

Sending Hello 6…

Received World 6

Sending Hello 7…

Received World 7

Sending Hello 8…

Received World 8

Sending Hello 9…

Received World 9

자, 이제 내용을 살펴보자. 무슨일이 일어났던 것일까?

ZeroMQ 에서 사용되는 모든 소켓 연결은 패턴으로 결합된다.

패턴이란, 우리가 흔히 사용하는 소켓을 이용한 데이터 전송 방식을 몇가지의 그룹으로 나뉘어 놓은 것을 이야기한다.

한가지 예를 들면, 흔히, 우리가 네트워크 프로그래밍을 한다면 client 가 요청하고 server 응답하는 방식을 생각한다.

즉, client가 먼저 질의(Request)하고, server가 응답(reply)하는 방식인데, 이를 Request-Reply 패턴이라 한다.

REQ-REP

fig2

REQ-REP 소켓 연결은 굉장히 엄격한 통신 방식이다. Client while 루프 안에서 zmq_send() 를 호출하고 zmq_recv 를 호출한다.

만약, zmq_send() 와 zmq_recv 사이에 다른 어떤 Sequence(예를 들어 zmq_send() 를 한번 더 호출한다던지..)하게 되면 해당 호출 결과는 에러처리될 것이다.

Server 역시 마찬가지로, 만약 zmq_recv 와 zmq_send 사이에 다른 Sequence 를 진행하게 되면, 에러로 처리된다.

보통의 소켓 프로그램에서 Server 보다 Client 가 먼저 실행될 경우, 어떤 현상이 나타나게 될지 생각해보자. 그리고 한번 위의 예제를 client 부터 실행을 해보자.

어떻게 되는가? 🙂

ZeroMQ String

ZeroMQ는 오직 전송되는 데이터의 사이즈만 확인한다. 이 말은 어떤 데이터 형식이라도 OK 이지만, 데이터 관리는 개발자 스스로가 해야 한다는 것이다.

가장 대표적인 예로 String 과 관련된 특징이 있다.

다음을 생각해보자.

만약 C 에서 NULL 바이트로 종료되는 문자열을 전송한다고 한다면, 아마 다음과 같이 입력할 수 있을 것이다.

zmq_send(requester, “Hello”, 6, 0);

하지만 만약 다른 언어에서라면 다르게 입력된 것이다. Python 을 예로 들어보자.

socket.send(“Hello”)

이런 경우(Python의 경우), 실제로 전송되는 데이터의 내용은 다음과 같다.

그리고, 만약 C 프로그램에서 이를 수신하게 된다면, 아마도 String 으로 보여지게 될 것이다. 하지만, 위의 그림에서 보듯이 문자열 종료 구분자(NULL)가 없다.

이는 사용자가 ZeroMQ 를 통하여 데이터를 송/수신할 때는 Server/Client 가 데이터 형식에 주의를 기울여야 함을 뜻한다.

fig3그렇다면, ZeroMQ 에서 String 문자열을 받기 위해서는 어떻게 해야 할까?

아래 예제 코드처럼 작성하면 된다.


// Receive 0MQ string from socket and convert into C string
// Chops string at 255 chars, if it's longer
static char* s_recv(
void* socket
)
{
char buffer[256];
int size = zmq_recv(socket, buffer, 255, 0);
if(size == −1)
return NULL;
if(size > 255)
size = 255;
buffer[size] = 0;
return strdup(buffer);
}

앞으로 등장하는 예제들은 대부분 위와 같은 개량된(?) 함수를 사용할 것이다. 위와 같은 함수들을 모아서 별도의 파일로 만든것이 zhelpers.h 파일이다. zhelpers.h 파일을 다운받기를 원한다면 http://zguide.zeromq.org/ 를 방문하면 된다.

Version Reporting

ZeroMQ는 많은 수의 버전을 가지고 있다. 만약, ZeroMQ를 사용하던 도중 문제가 발생했다면, 대부분은 이후의 버전에서 수정되어 있을 것이다. 현재 사용중인(Link 되어 있는) 정확한 ZeroMQ의 버전 정보는 다음의 코드로 확인이 가능하다.


// Report 0MQ version

#include

int main(void)
{
int major, minor, patch;
zmq_version(&major, &minor, &patch);
printf("Current 0MQ version is %d.%d.%dn", major, minor, patch);
return 0;
}

프로그램을 실행해보면 다음과 같이 나온다.

Current 0MQ version is 3.2.3

Getting the Message Out

두번째로 다룰 패턴은 하나의 Server와 다수개의 Client로 이루어지는 One-way(일방향) 데이터 배포 패턴이다. 예를 들어 우편 번호, 온도, 습도 를 포함하는 날씨 정보를 배포하는 시스템을 만들어보자. 진짜 기상 센터가 하는 것처럼 날씨 정보도 랜덤하게 만들어 보자.

Server쪽 코드이다. 5556 포트를 사용한다.


// Weather update server
// Bind PUB socket to tcp://*:5556
// Publishes random weather updates

#include "zhelpers.h"

int main(int argc, char **argv)
{
// Prepare our context and publisher
void* context = zmq_ctx_new();
void* publisher = zmq_socket(context, ZMQ_PUB);
int rc;

rc = zmq_bind(publisher, "tcp://*:5556");
assert(rc == 0);

rc = zmq_bind(publisher, "ipc://weather.ipc");
assert(rc == 0);

// Initialize random number generator
srandom((unsigned)time(NULL));
while(1)
{
// Get values that will fool the boss
int zipcode, temperature, relhumidity;
zipcode = randof(100000);
temperature = randof(215) − 80;
relhumidity = randof(50) + 10;

// Send message to all subscribers
char update[20];
sprintf(update, "%05d %d %d", zipcode, temperature, relhumidity);
s_send(publisher, update);
}

zmq_close(publisher);
zmq_ctx_destroy(context);

return 0;

}

시작도 끝도 없는 Broadcasting 이다. 그냥 무조건 데이터를 전송한다.

이제 Client 를 작성해보자. Client의 요지는 모든 데이터에 주의를 기울이되, 특정 zip code(우편번호) 데이터만 수신하는 것이다. 기본으로 뉴욕시의 우편번호를 사용하도록 하겠다. 뉴욕은 뭔가 모험을 시작하기에 좋은 장소이므로.. ㅎㅎ


// Weather update client
// Connects SUB socket to tcp://localhost:5556
// Collects weather updates and finds avg temp in zipcode

#include "zhelpers.h"

int main(int argc, char** argv)
{
// Socket to talk to server
printf("Collecting updates from weather server...n");
void* context = zmq_ctx_new();
void* subscriber = zmq_socket(context, ZMQ_SUB);
int rc;

rc = zmq_connect(subscriber, "tcp://localhost:5556");
assert(rc == 0);

// Subscriber to zipcode, default is NYC, 10001
char* filter = (argc > 1) ? argv[1]:"10001";
rc = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter));
assert(rc == 0);

// Process 100 updates
int update_nbr;
long total_temp = 0;
for(update_nbr = 0; update_nbr < 100; update_nbr++) { char* string = s_recv(subscriber); int zipcode, temperature, relhumidity; sscanf(string, "%d %d %d", &zipcode, &temperature, &relhumidity); printf("[%d]: %sn", update_nbr, string); total_temp += temperature; free(string); } printf("Average temperature for zipcode '%s' was %dFn", filter, (int)(total_temp / update_nbr)); zmq_close(subscriber); zmq_ctx_destroy(context); return 0; }

실행하면 아래와 같이 나온다.

Collecting updates from weather server...

[0]: 10001 -20 32

[1]: 10001 -51 11

[2]: 10001 9 13

...

[96]: 10001 -35 21

[97]: 10001 82 52

[98]: 10001 43 53

[99]: 10001 -39 56

Average temperature for zipcode '10001' was 37F

fig4한가지 염두해두어야 하는 점은 SUB 소켓을 사용할 때는, 반드시 zmq_setsockopt() 와 SUBSCRIBE 지시자를 코드안에 입력하여 이용하여 수신할 데이터를 설정해야 한다는 것이다. 만약 수신할 데이터(subscription)을 지정하지 않는다면, 그 어떤 메시지도 수신할 수 없다. 이는 이제 막 ZeroMQ 를 사용하기 시작하는 사람들이 하는 흔한 실수이다. 하나의 구독 지시자(Subscriber)는 한번에 여러개의 수신할 데이터(Subscription) 지정이 가능하다. 물론, Subscription 지정 취소도 가능하다. 그리고 Subscription 은 반드시 String 이 아니어도 상관없다. 자세한 설정은 zmq_setsockopt() 설명을 참고 바란다.

PUB-SUB 소켓은 비동기로 작동한다. Client 는 그저 zmq_recv() 만을 수행할 뿐이다. 만약 SUB 소켓을 통해 send 를 하고자 한다면, 에러가 발생한다. 그리고 Server 의 경우, zmq_send() 를 통해 데이터 전송만을 할 뿐이다. 마찬가지로, PUB 소켓으로 zmq_recv() 를 한다면 에러가 발생한다.

ZeroMQ에서는 누가 connect 를 수행하고, 누가 bind 를 수행하는지는 중요하지 않다. 하지만 실제적으로는 다른점이 있다. 그건 나중에 다루어보도록 하고... 지금은 bind 는 PUB 가 수행하고, connect 는 SUB가 수행한다고만 알아두자.

PUB-SUB 소켓에 대해서 한가지 더 알아야 할 중요한 사실이 있다. Publisher 에서는 누가 언제부터 구독을 시작했는지 모른다는 점이다. 그리고 만약  Subscriber 를 먼저 시작하고, Publisher 를 나중에 시작했다 하더라도 Publisher 가 전송하는 첫번째 메시지는 항상 수신하지 못할 것이다. Subscriber 가 Publisher 에게 접속을 하고 구독을 시작하는 시간이 매우 짧다고는 하나 0초가 아니기 때문이다. 그리고, 그 짧은 시간동안 Publisher는 소켓으로 이미 데이터를 전송했기 때문이다.

이를 "slow joiner"라고 표한다. ZeroMQ 는 Background 에서 동작하는 비동기 I/O라는 점을 기억하라.

Using wsdl files on C

회사 프로젝트로 wsdl 파일을 이용한 웹 서비스 프로그램을 제작해야 할 일이 있었다.

물론 사용 언어는 C.
먼저 wsdl 파일들에서 header 파일을 추출해야 했다.

이를 위해서 사용한 프로그램은 wsdl2h

 3개의 wsdl 파일에서 header 파일을 추출해야 했다.

다음의 명령어를 사용했다.

$ wsdl2h -c -o SoapEnv.h SendSms.wsdl

$ wsdl2h -c -n SendSms -o SendSms.h SendSms.wsdl

$ wsdl2h -c -n ReceiveSms -o ReceiveSms.h ReceiveSms.wsdl

$ wsdl2h -c -n SmsNotificationManagerService -o SmsNotificationManagerService.h SmsNotificationManagerService.wsdl

위의 명령어를 잘 보면 추출해야 하는 wsdl 파일은 3개인데, 사용한 명령어는 총 4개이다.
정확히는 SendSms.wsdl 파일에서 두개의 header 파일을 추출했다.

 이유는 실제 컴파일과 라이브러리 구현에 사용할 gsoap를 위해서 기본 header 파일이 필요했기 때문이다. 
그리고, 위에 나타낸 옵션 중, −c 옵션은 c 언어를 위한 헤더 파일 생성 옵션, −n 옵션은 namespace 영역을 구분짓기 위해 사용한 옵션이다.

하나 이상의 wsdl 파일을 사용한 라이브러리 제작시, −n 옵션이 특히 중요한데, 이유는 −n 옵션 없이 wsdl2h 를 사용할 경우, 여러개의 wsdl 사용시 서로 혼합된 namespace 영역을 사용하기 때문이다.
이는 나중에 wsdl 파일을 추가 하게 될 경우, 프로그램 코드상에서 호출되는 메소드 이름이 변경이 되는 등 여러모로 곤란이 생긴다. 때문에 반드시 필요한 옵션이다.
ref: http://www.cs.fsu.edu/~engelen/soapdoc2.html#tth_sEc8

wsdl2h 를 사용하면 각각의 헤더 파일들이 만들어 진다. 이렇게 만들어진 헤더파일들을 이용해 실제 C 에서 사용 가능한 코드로 만들어줘야 한다.이를 해주는 프로그램이 soapcpp2 이다. 이번 프로젝트에서 다음의 명령어/옵션을 사용했다.

$ soapcpp2 -C -c SoapEnv.h

$ soapcpp2 -C -c -n -pSendSms SendSms.h

$ soapcpp2 -C -c -n -pReceiveSms ReceiveSms.h

$ soapcpp2 -C -c -n -pSmsNotificationManagerService SmsNotificationManagerService.h

나는 이 명령어를 makefile 안에 넣어 두었다. 그리고 매번 컴파일을 실행할 때마다 위의 명령어가 자동으로 시작되도록 구성했다.
왜냐하면 soapcpp2 프로그램의 버전때문이었다. 

soapcpp2 프로그램으로 생성되는 파일들은 바로 C 로 컴파일이 가능한 *.c 파일과 *.h 파일들이다.이를 이용해서 라이브러리를 생성하고, 링크시켜서 사용하기 위해서는 libgsoap.a 파일과 같은 gsoap 라이브러리가 추가로 필요하다.
그런데, 만약 매번 soapcpp2 프로그램을 통해서 *.c 와 *.h 파일을 생성하지 않는다면, 이미 생성되어있는 파일들을 이용해서 라이브러리를 만들고 gsoap 라이브러리와 링크를 할텐데..
 때, *.c/*.h 파일을 생성한 soapcpp2 의 버전과 링크되는 libgsoap.a 파일의 버전이 서로 맞지 않는다면 링크가 안되는 문제가 발생한다.
이런 이유로 매번 컴파일을 진행할 때마다 위의 명령어를 통해서 새로이 stub 코드를 작성하도록 구성했다.

그리고, wsdl2h 사용할 때와 마찬가지로, namespace 영역 구분을 위해 −n 옵션과 −p 옵션을 추가했다. 그리고, gcc 컴파일을 진행시 −D 옵션을 이용해 Define 을 별도로 걸어주었다.
이는 지정된 namespace 영역을 사용하기 위해 필요한 조치이다.
makefile 안에 다음과 같은 Define 을 넣어주었다.ref: http://www.cs.fsu.edu/~engelen/soapdoc2.html#tth_sEc19.36

DEFS                    = -DWITH_OPENSSL -DWITH_NONAMESPACES

마지막으로, 왜 그런지는 모르겠는데..  이상하게 gsoap 라이브러리를 추가시, 자꾸 세그먼트 폴트가 났다.
계속 방법을 찾다가 제일 마지막에 gsoap 라이브러리를 추가 해야 오류가 나지않는 이상한 현상을 확인했다.
왜그러는지는 모르겠으나.. 다른 해결할 방법을 찾지 못했다.

LIBS            =  -lzmq -lczmq -luuid -ljansson -lssl -lcrypto -lpthread -lcurl -lgsoapssl -lgsoap

pmake error on ubuntu

ubuntu 에서 pmake 를 사용하던 도중, 아래의 오류가 나타났다.

library 를 컴파일하는 과정이었는데, 필요한 library는 모두 만들어 놓고, 아래의 오류를 나타내고 죽어버렸다.

building shared object ipvutil library

ranlib libipvutil_pic.a

lint -chapbxzF  -i strlcat.c

pmake: exec(lint) failed (No such file or directory)

lint 프로그램을 찾을 수 없다는 메시지였는데.. lint 가 무슨 프로그램인지 알 수 없었다. 결국 구글링..

http://en.wikipedia.org/wiki/Lint_%28software%29

즉, lint 는 일종의 cppcheck 와 같은 코드 검사기 같은 것이다.

하지만 lint는 bsd-unix 와 같은 unix 계열의 os 에서 주로 사용되는 검사기였다. 즉, ubuntu에는 기본이 아니라는 이야기..

lint 와 같은 역할을 하는 것으로 linux 에서는 splint 가 있어서 설치 한 뒤, ln 명령어로 lint 명령을 사용할 수 있도록 구성했으나, splint 및 lint 의 정확한 사용법을 모르는 상태에서  기본적으로 사용되는 옵션등이 맞지 않아 계속 오류를 나타냈다.

결국 lint 검사를 사용하지 않도록 하고 mk 파일 내용을 수정하는 것으로 해결을 했다.

/usr/share/mk 디렉토리에 있는 bsd.own.mk 파일을 다음과 같이 수정하도록 하자. 굵은 글씨로 나타낸 부분이 lint 사용을 막기 위해 추가한 부분이다.

#       $NetBSD: bsd.own.mk,v 1.120 1999/02/24 14:42:36 drochner Exp $

.if !defined(_BSD_OWN_MK_)

_BSD_OWN_MK_=1

.if defined(MAKECONF) && exists(${MAKECONF})

.include “${MAKECONF}”

.elif exists(/etc/mk.conf)

.include “/etc/mk.conf”

.endif

NOLINT=

Installing bsdmake on ubuntu

프로젝트를 진행하면서 bsd-make(bmake)를 사용할 일이 생겼다.

당연히 ubuntu에서 사용가능한 bmake package 를 찾아봤으나… 아무래도 보이지 않았다.

한참을 찾고 찾아도 도저히 보이지 않아 결국, Chris 에게 도움을 요청했다.

내 설명을 들은 Chris가 구글에서 debian bmake 를 검색하니 바로 나왔다..(그동안 나는 ubuntu 와 bmake 키워드를 가지고 계속 삽질을 하고 있었다.)

결국 너무 늦은시간이라 회사에서는 해결을 못하고, 집에와서 다시금 찾아보니 답이 나왔다.

정답은…

$ sudo apt-get install pmake

pmake 였던 것이다.. -_-;;

$ man pmake

MAKE(1)                                                                                                BSD General Commands Manual                                                                                                MAKE(1)

NAME

     pmake — maintain program dependencies

SYNOPSIS

     pmake [-BeikNnqrstWX] [-D variable] [-d flags] [-f makefile] [-I directory] [-J private] [-j max_jobs] [-m directory] [-T file] [-V variable] [variable=value] [target …]

DESCRIPTION

     pmake is a program designed to simplify the maintenance of other programs.  Its input is a list of specifications as to the files upon which programs and other files depend.  If the file ‘makefile’ exists, it is read for this

     list of specifications.  If it does not exist, the file ‘Makefile’ is read.  If the file ‘.depend’ exists, it is read (see mkdep(1)).

     This manual page is intended as a reference document only.  For a more thorough description of pmake and makefiles, please refer to Make – A Tutorial.

Installing gsoap-2.8 in CentOS 6.4

이번에 새로 입사한 회사에서 CentOS를 기반으로 작업을 하는 관계로… 오랫만에 CentOS 에서 작업을 하게 되었다.

처음 셋팅을 하면서 여러번의 삽질이 있었는데.. 그중에서도 gsoap 설치와 관련하여 삽질이 있어 여기에 포스팅을 한다.

이번에 gsoap-2.8 을 소스 설치를 하게 되었는데.. 결론은 다음의 패키지 항목들이 필요했다.

yum install gcc gcc-c++ flex* byacc* bison*