WebRTC 미디어 서버인 Janus는 모듈화된 구조를 통해 다양한 비즈니스 로직을 유연하게 확장할 수 있는 설계를 채택하고 있다. 본문에서는 Janus의 핵심 메커니즘인 플러그인 로딩 방식과 런타임 관리 원리를 살본다.
Janus 계층 구조
Janus는 명확한 두 계층으로 분리되어 있다. 코어 계층은 스레드 풀 관리, 네트워크 이벤트 처리, WebRTC 프로토콜 스택 구현 등 인프라 기능을 담당한다. 반면 플러그인 계층은 실제 미디어 처리 로직, 시그널링 명령 해석, 비즈니스 규칙 적용 등을 수행한다.
이러한 분리는 빈번하게 변경되는 요구사항에 신속히 대응할 수 있게 한다. 새로운 기능이 필요할 때 기존 코드를 수정하지 않고 독립적인 플러그인을 작성하여 배포하면 된다.
동적 라이브러리 로딩 메니즘
Janus의 플러그인 시스템은 POSIX 표준인 dlopen과 dlsym 인터페이스에 기반한다. 이 API들을 이해하면 Janus의 내부 동작을 쉽게 파악할 수 있다.
#include <dlfcn.h>
/* 공유 라이브러리를 메모리에 적재 */
void* dlopen(const char* filepath, int flags);
/* 적재된 라이브러리 내 심볼 주소 획득 */
void* dlsym(void* handle, const char* symbol_name);
dlopen의 두 번째 인자는 바인딩 시점을 제어한다. RTLD_LAZY는 실제 참조 시점에 바인딩하고, RTLD_NOW는 즉시 모든 볼을 해석한다. Janus는 후자를 사용하여 런타임 심볼 오류를 방지한다.
동적 로딩 예제
간단한 산술 연산 라이브러리를 작성하여 동적 로딩 과정을 확인해 보자.
라이브러리 소스 (math_ops.c):
#include <stdio.h>
int accumulate(int x, int y, int z) {
return x + y + z;
}
/* 컴파일 명령: gcc -fPIC -shared -o libmath.so math_ops.c */
로더 구현 (loader.c):
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
typedef int (*triadic_func)(int, int, int);
int main(void) {
void *lib_handle = dlopen("./libmath.so", RTLD_NOW | RTLD_LOCAL);
if (!lib_handle) {
fprintf(stderr, "라이브러리 로딩 실패: %s\n", dlerror());
return EXIT_FAILURE;
}
triadic_func calc = (triadic_func)dlsym(lib_handle, "accumulate");
if (!calc) {
fprintf(stderr, "심볼 해석 실패: %s\n", dlerror());
dlclose(lib_handle);
return EXIT_FAILURE;
}
int outcome = calc(5, 10, 15);
printf("연산 결과: %d\n", outcome);
dlclose(lib_handle);
return EXIT_SUCCESS;
}
Janus 플러그인 로딩 흐름
Janus는 지정된 디렉터리를 순회하여 공유 객체 파일을 발견하면 즉시 메모리에 적재한다. 핵심 로직은 다음과 같이 단순화할 수 있다.
DIR *plugin_dir = opendir(configured_path);
struct dirent *entry;
while ((entry = readdir(plugin_dir)) != NULL) {
if (!is_valid_plugin_file(entry->d_name))
continue;
char full_path[PATH_MAX];
snprintf(full_path, sizeof(full_path), "%s/%s",
configured_path, entry->d_name);
void *module = dlopen(full_path, RTLD_NOW | RTLD_GLOBAL);
if (!module)
continue;
/* 팩토리 함수 획득 */
janus_plugin* (*instantiate)(void);
instantiate = dlsym(module, "create");
if (instantiate) {
janus_plugin *instance = instantiate();
register_plugin(instance);
}
}
각 플러그인은 반드시 create라는 이름의 팩토리 함수를 노출해야 한다. 이 함수는 janus_plugin 구조체를 반환하며, 해당 구조체에는 다양한 콜백 함수 포인터가 포함된다.
필수 구현 인터페이스
플러그인 개발자가 반드시 구현해야 하는 메서드 목록은 다음과 같다.
| 메서드 | 역할 |
|---|---|
init | 설정 파일 파싱, 내부 자원 초기화 |
destroy | 정리 작업 및 메모리 반환 |
get_api_compatibility | Janus 코어와의 API 버전 호환성 검증 |
get_version | 숫자 형태 버전 정보 |
get_version_string | 가독성 있는 버전 문자열 |
get_description | 기능 설명 문서화 |
get_name | 간결한 식별자 |
get_package | 유일한 네임스페이스 (예: janus.plugin.voicemail) |
create_session | 클라이언트 세션 객체 생성 |
handle_message | JSON 시그널링 메시지 처리 |
handle_admin_message | 관리 API 명령 처리 |
setup_media | 미디어 전송 채널 준비 |
slow_link | 네트워크 품질 저하 알림 처리 |
hangup_media | 연결 종료 이벤트 처리 |
query_session | 런타임 세션 상태 조회 |
destroy_session | 세션 자원 해제 |
다음 세 가지는 선택적 구현이다.
incoming_rtp: 수신 RTP 패킷 가로채기incoming_rtcp: 수신 RTCP 피드백 처리incoming_data: SCTP DataChannel 데이터 수신data_ready: DataChannel 전송 가능 상태 확인
필수 인터페이스를 모두 구현하면 Janus 코어는 해당 플러그인을 정상적인 컴포넌트로 인식하고, 시그널링 흐름에 통합하여 동작시킨다.