메뉴 건너뛰기

Hodol's Blog

Libwebsockets을 이용한 웹소켓 서버 프로그래밍, 간단한 채팅 사이트 만들기

목차
라이브러리 설치
yum에서 검색해보니 패키지가 없다. 소스 컴파일로 설치를 해야 한다. 다음 주소에서 최신 버전 (현재 libwebsockets-1.4-chrome43-firefox-36.tar.gz)를 다운 받아서 압축을 풀면, README.build.md 파일에 설치 방법이 적혀 있다. 보고 따라 하면 된다.

http://git.libwebsockets.org/cgi-bin/cgit/libwebsockets/

다만 몇 가지 주의 사항을 기술한다.
  • CMake를 이용하여 컴파일 한다. 따라서 cmake가 설치되어 있어야 한다. yum이나 apt-get으로 쉽게 설치할 수 있다.
  • gcc 뿐만 아니라 gcc-c++도 필요하다. 역시 설치되지 않았다면 설치하도록 하자. 이 외에도 zlib-devel 같은 패키지들은 없으면 설치 도중에 not found... 메시지를 출력하는데 설치에 지장은 없다. 라이브러리 제작자가 직접 확인해준 사항이다. 오오, 창조자 클라스, 오오!
  • 64비트 운영체제라면 64비트로 설치해야 한다. cmake -DLIB_SUFFIX=64 처럼 추가 옵션과 함께 명령을 입력한다. 그 외, 라이브러리가 설치될 디렉토리를 따로 지정하거나 OpenSSL과 함께 설치하는 등의 옵션이 있으니 README.build.md 파일을 꼼꼼히 읽어 보자.
예제
선행 예제
필자는 초보자라 libwebsockets 라이브러리에 대한 자세한 설명을 하기 어렵다. 다음 사이트를 방문하여 간단한 예제를 먼저 살펴 보기를 권한다.
http://ahoj.io/libwebsockets-simple-websocket-server
문자열을 입력하면 웹소켓 서버가 이를 받아 거꾸로 출력하는 에코 서버 예제이다. 이 게시물의 예제도 이 사이트의 예제를 변형한 것이다.
한 가지 주의해야 할 부분이 있다 : 라이브러리 버전이 업데이트 되면서 사이트 예제의 main() 함수의 앞부분의 내용을 다음과 같이 변경해야 한다. 사이트 아래쪽에 댓글에서도 언급된 내용이다.
int main(void) {
    // server url will be http://localhost:9000
    int port = 9000;
    const char *interface = NULL;
    struct libwebsocket_context *context;
    // we're not using ssl
    const char *cert_path = NULL;
    const char *key_path = NULL;
    // no special options
    int opts = 0;
    struct lws_context_creation_info info;

    memset(&info,0,sizeof(info));
    info.port=port;
    info.iface=interface;
    info.protocols = protocols;
    info.extensions = NULL; // libwebsocket_get_initernal_extensions();
    info.ssl_cert_filepath=NULL;
    info.ssl_private_key_filepath=NULL;
    info.gid=-1;
    info.uid=-1;
    info.options=opts;
    // create libwebsocket context representing this server
    context = libwebsocket_create_context(&info);
...
비교해보면 초기화 과정에서 변수들이 구조체 멤버로 바뀌었다.
채팅 서버 예제
앞의 에코 서버는 클라이언트와 서버가 일대일로 통신하는 것이다. 채팅 서버는 많은 클라이언트들과 서버가 다대일 통신을 해야 한다. 선행 예제와의 차이점을 미리 요약하자면, switch문의 case LWS_CALLBACK_RECEIVE: 부분의 주요 내용이 case LWS_CALLBACK_SERVER_WRITEABLE: 부분으로 옮겨 갔으며, LWS_CALLBACK_RECEIVE에는 libwebsocket_callback_on_writable_all_protocol() 함수를 호출하는 코드가 추가되었다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libwebsockets.h>
libwebsockets.h를 포함하였다.
static int callback_http(struct libwebsocket_context *this,
        struct libwebsocket *wsi,
        enum libwebsocket_callback_reasons reason, void *user,
        void *in, size_t len)
{
    return 0;
}
http 프로토콜을 받았을 때는 아무것도 하지 않는다. 여기서 프로토콜은 '네트워크 규약'이라는 의미보다 '통신 채널' 또는 '통신 주파수' 정도로 이해는게 알맞은 듯 하다.
struct a_message {
    void* payload;
    size_t len;
};
클라이언트의 메세지가 담길 구조체이다.
static struct a_message messageRecieved;
클라이언트의 메세지가 담길 구조체 변수를 선언했다. 한 클라이언트가 메세지를 보내면 접속한 모든 클라이언트들에게 메세지를 보내어야 하므로 외부 정적 변수로 선언하였다. 사실 이부분은 제대로 구현되지 못한 것이, 여러 클라이언트가 서버의 응답속도보다 빠르게 메세지들을 보낼 경우 이전에 보내어진 메세지 위에 나중에 보내어진 메세지가 덧씌워져 이전 메세지를 잃어버리게 된다. 2-3개의 클라이언트 정도로는 괜찮은 듯하고 실습 예제이니 그냥 넘어가도록 하자.
static int callback_dumb_increment(struct libwebsocket_context *this,
        struct libwebsocket *wsi,
        enum libwebsocket_callback_reasons reason,
        void *user, void *in, size_t len)
{
뒤에 언급되지만, 우리가 사용할 프로토콜 이름은 dumb-increment-protocol이고 이 프로토콜로 메세지를 수신하면 콜백 함수로 callback_dumb_increment 함수를 호출한다.
    switch (reason) {
        case LWS_CALLBACK_ESTABLISHED: // just log message that someone is connecting
            printf("connection established\n");
            break;
연결이 되면 실행되는 부분이다. 단순히 로그만 출력한다.
        case LWS_CALLBACK_SERVER_WRITEABLE:
            printf("callback received\n");
            if (messageRecieved.len <= 0)
            break;

            unsigned char *buf = (unsigned char*) malloc(LWS_SEND_BUFFER_PRE_PADDING + messageRecieved.len + LWS_SEND_BUFFER_POST_PADDING);

            int i;
            for (i=0; i < messageRecieved.len; i++) {
                buf[i+LWS_SEND_BUFFER_PRE_PADDING] = ((unsigned char *)messageRecieved.payload)[i+LWS_SEND_BUFFER_PRE_PADDING];
            }

            printf("received data: %s, replying: %.*s\n", (unsigned char *) messageRecieved.payload + LWS_SEND_BUFFER_PRE_PADDING, (int) messageRecieved.len, buf + LWS_SEND_BUFFER_PRE_PADDING);

            libwebsocket_write(wsi, &buf[LWS_SEND_BUFFER_PRE_PADDING], messageRecieved.len, LWS_WRITE_TEXT);
            free(buf);

            break;
서버가 메세지를 쓸(뿌릴) 준비가 되면 호출되는 부분이다. 클라이언트로 받은 메세지 messageRecieved.payloadbuf에 형식에 맞춰 복사하여, libwebsocket_write() 함수로 보낸다. 이 부분은 복수의 클라이언트들이 접속한 경우, 아래의 libwebsocket_callback_on_writable_all_protocol() 함수에 의해 여러번 호출된다.
        case LWS_CALLBACK_RECEIVE:{

            if(messageRecieved.payload)
                free(messageRecieved.payload);

            messageRecieved.payload = malloc(LWS_SEND_BUFFER_PRE_PADDING + len + LWS_SEND_BUFFER_POST_PADDING);
            messageRecieved.len = len;
            memcpy((unsigned char *)messageRecieved.payload + LWS_SEND_BUFFER_PRE_PADDING,in,len);

            printf("messageRecieved.payload : %s \n messageRecieved.len : %d\n",(unsigned char *)messageRecieved.payload+LWS_SEND_BUFFER_PRE_PADDING,(int) messageRecieved.len);
            libwebsocket_callback_on_writable_all_protocol(libwebsockets_get_protocol(wsi));
            break;
        }
서버가 클라이언트로부터 메세지를 받으면 호출되는 부분이다. 콜백 함수 인수인 in에 저장된 클라이언트 메세지를 외부 정적 변수(구조체 멤버)인 messageRecieved.payload에 복사한다. 복사 작업이 완료되면 libwebsocket_callback_on_writable_all_protocol() 함수를 호출하여 서버가 메세지를 전송할 준비를 하게 한다. libwebsockets_get_protocol(wsi) 부분은 wsi 구조체에 기록된 dumb_increment 프로토콜을 사용하는 클라이언트들의 정보는 뽑아낸다.
        default:
            break;
        }

    return 0;
}
모든 작업이 끝나면 콜백 함수를 종료시킨다.

static struct libwebsocket_protocols protocols[] = {
    /* first protocol must always be HTTP handler */
    {
        "http-only", // name
        callback_http, // callback
        0 // per_session_data_size
    },
    {
        "dumb-increment-protocol", // protocol name - very important!
        callback_dumb_increment, // callback
        0 // we don't use any per session data

    },
    {
        NULL, NULL, 0 /* End of list */
    }
};
프로토콜 관련 정보들이다. 첫째 멤버는 관례적으로 http-only를 고정하여 쓴다. 두번째 멤버가 우리가 사용할 dumb-increment-protocol 이름의 프로토콜이며 콜백 함수로 callback_dumb_increment 함수가 등록된다. 마지막 멤버는 NULL을 넣어줌으로써 프로토콜 리스트의 종료를 알린다.
int main(void) {
    // server url will be http://localhost:9000
    int port = 9000;
    const char *interface = NULL;
    struct libwebsocket_context *context;
    // we're not using ssl
    const char *cert_path = NULL;
    const char *key_path = NULL;
    // no special options
    int opts = 0;
    struct lws_context_creation_info info;

    memset(&info,0,sizeof(info));
    info.port=port;
    info.iface=interface;
    info.protocols = protocols;
    info.extensions = NULL; // libwebsocket_get_initernal_extensions();
    info.ssl_cert_filepath=NULL;
    info.ssl_private_key_filepath=NULL;
    info.gid=-1;
    info.uid=-1;
    info.options=opts;
서버 초기화 관련 정보를 lws_context_creation_info 구조체에 담는다. 포트 번호가 9000이다. 방화벽을 열어주는 것을 잊지 말자!
    // create libwebsocket context representing this server
    context = libwebsocket_create_context(&info);

    if (context == NULL) {
        fprintf(stderr, "libwebsocket init failed\n");
        return -1;
    }
libwebsocket_create_context() 함수로 서버 정보를 초기화 한다. 만약 에러가 발생하면 함수를 종료한다.
    printf("starting server...\n");

    // infinite loop, to end this server send SIGTERM. (CTRL+C)
    while (1) {
        libwebsocket_service(context, 50);
        // libwebsocket_service will process all waiting events with their
        // callback functions and then wait 50 ms.
        // (this is a single threaded webserver and this will keep our server
        // from generating load while there are not requests to process)
    }
무한 반복문을 통해 libwebsocket_service() 함수를 반복 호출하여 서버를 작동 시킨다. libwebsocket_service() 함수의 두번째 인자는 메세지 수신을 주기적으로 확인하는 시간 간격이다.
    libwebsocket_context_destroy(context);

    return 0;
}
함수를 종료할 때, 동적으로 할당한 메모리를 해제해준다. 하지만 Ctrl+C로 종료할테니 이 코드가 실행될 일은 없....
전체 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libwebsockets.h>

static int callback_http(struct libwebsocket_context *this,
        struct libwebsocket *wsi,
        enum libwebsocket_callback_reasons reason, void *user,
        void *in, size_t len)
{
    return 0;
}

struct a_message {
    void* payload;
    size_t len;
};
static struct a_message messageRecieved;
static int callback_dumb_increment(struct libwebsocket_context *this,
        struct libwebsocket *wsi,
        enum libwebsocket_callback_reasons reason,
        void *user, void *in, size_t len)
{

    switch (reason) {
        case LWS_CALLBACK_ESTABLISHED: // just log message that someone is connecting
            printf("connection established\n");
            break;

        case LWS_CALLBACK_SERVER_WRITEABLE:
            printf("callback received\n");
            if (messageRecieved.len <= 0)
            break;

            unsigned char *buf = (unsigned char*) malloc(LWS_SEND_BUFFER_PRE_PADDING + messageRecieved.len + LWS_SEND_BUFFER_POST_PADDING);

            int i;
            for (i=0; i < messageRecieved.len; i++) {
                buf[i+LWS_SEND_BUFFER_PRE_PADDING] = ((unsigned char *)messageRecieved.payload)[i+LWS_SEND_BUFFER_PRE_PADDING];
            }

            printf("received data: %s, replying: %.*s\n", (unsigned char *) messageRecieved.payload + LWS_SEND_BUFFER_PRE_PADDING, (int) messageRecieved.len, buf + LWS_SEND_BUFFER_PRE_PADDING);

            libwebsocket_write(wsi, &buf[LWS_SEND_BUFFER_PRE_PADDING], messageRecieved.len, LWS_WRITE_TEXT);
            free(buf);

            break;

        case LWS_CALLBACK_RECEIVE:{

            if(messageRecieved.payload)
                free(messageRecieved.payload);

            messageRecieved.payload = malloc(LWS_SEND_BUFFER_PRE_PADDING + len + LWS_SEND_BUFFER_POST_PADDING);
            messageRecieved.len = len;
            memcpy((unsigned char *)messageRecieved.payload + LWS_SEND_BUFFER_PRE_PADDING,in,len);

            printf("messageRecieved.payload : %s \n messageRecieved.len : %d\n",(unsigned char *)messageRecieved.payload+LWS_SEND_BUFFER_PRE_PADDING,(int) messageRecieved.len);
            libwebsocket_callback_on_writable_all_protocol(libwebsockets_get_protocol(wsi));
            break;
        }
        default:
            break;
        }

    return 0;
}


static struct libwebsocket_protocols protocols[] = {
    /* first protocol must always be HTTP handler */
    {
        "http-only", // name
        callback_http, // callback
        0 // per_session_data_size
    },
    {
        "dumb-increment-protocol", // protocol name - very important!
        callback_dumb_increment, // callback
        0 // we don't use any per session data

    },
    {
        NULL, NULL, 0 /* End of list */
    }
};
int main(void) {
    // server url will be http://localhost:9000
    int port = 9000;
    const char *interface = NULL;
    struct libwebsocket_context *context;
    // we're not using ssl
    const char *cert_path = NULL;
    const char *key_path = NULL;
    // no special options
    int opts = 0;
    struct lws_context_creation_info info;

    memset(&info,0,sizeof(info));
    info.port=port;
    info.iface=interface;
    info.protocols = protocols;
    info.extensions = NULL; // libwebsocket_get_initernal_extensions();
    info.ssl_cert_filepath=NULL;
    info.ssl_private_key_filepath=NULL;
    info.gid=-1;
    info.uid=-1;
    info.options=opts;

    // create libwebsocket context representing this server
    context = libwebsocket_create_context(&info);

    if (context == NULL) {
        fprintf(stderr, "libwebsocket init failed\n");
        return -1;
    }

    printf("starting server...\n");

    // infinite loop, to end this server send SIGTERM. (CTRL+C)
    while (1) {
        libwebsocket_service(context, 50);
        // libwebsocket_service will process all waiting events with their
        // callback functions and then wait 50 ms.
        // (this is a single threaded webserver and this will keep our server
        // from generating load while there are not requests to process)
    }

    libwebsocket_context_destroy(context);

    return 0;
}
컴파일 및 실행
$ gcc websocketserver.c -o websocketserver -lwebsockets
$ ./websocketserver
다시 한번 언급하지만 방화벽 설정에서 포트를 열고 서버를 실행하자.
클라이언트 코드
ws://127.0.0.1:9000 이라고 된 부분은 서버 주소로 알맞게 고쳐서 사용해야 한다.
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
        <script type="text/javascript">
            $(function() {
                window.WebSocket = window.WebSocket || window.MozWebSocket;

                var websocket = new WebSocket('ws://127.0.0.1:9000', // <---- 요기 수정
                    'dumb-increment-protocol');

                websocket.onopen = function () {
                    $('h1').css('color', 'green');
                };

                websocket.onerror = function () {
                    $('h1').css('color', 'red');
                };

                websocket.onmessage = function (message) {
                    console.log(message.data);
                    $('div').append($('<p>', { text: message.data }));
                };


                $('button').click(function(e) {
                    e.preventDefault();
                    websocket.send($('input').val());
                    $('input').val('');
                });
            });
        </script>
    </head>
    <body>
        <h1>WebSockets test</h1>
        <form>
            <input type="text" />
            <button>Send</button>
        </form>
        <div></div>
    </body>
</html>
참고