English | 한국어
Core 스펙 목차 | 이전: Context | 다음: Errors
Message¶
이 장이 정의하는 것 — message lifecycle, routing ID와 ownership의 공개 계약.
1. Message 개요¶
zlink의 message는 socket 사이에서 임의의 binary payload를 전달하는 기본 단위다. message가 운반하는 사용자 data byte를 payload라 한다. message는 data를 복사하지 않고 pointer·참조만 전달해 전송하는 zero-copy 방식과, 여러 frame(part)을 하나의 논리적 message로 묶는 multipart를 지원한다.
이 문서는 message의 생성, payload 접근, ownership과 multipart 배열의 공개 계약을 정의한다. 대상 독자는 message lifecycle과 zero-copy buffer ownership을 C API와 각 언어 binding으로 옮기는 개발자다. 이 문서는 "socket이 송수신하는 message를 어떻게 만들고 공유하며 정확히 한 번 해제하는가?"에 답한다.
공개 message API는 payload part의 container다. message-level request-reply 함수를 제공하지 않고, per-message metadata 값도 현재 노출하지 않으며, request-reply 또는 socket routing 상태를 노출하지 않는다. request-reply와 peer 상세 정보는 message API가 아니라 socket 공개 계약이 제공한다.
관련 계약의 소유 문서는 다음과 같다.
| 관련 계약 | 정의하는 문서 |
|---|---|
| request-reply·routing과 peer 상세 정보 | Socket 공통과 각 socket 정식 문서 |
| Context 수명과 옵션 | Context |
2. Message lifecycle¶
message의 수명은 초기화 → 사용 → close 순으로 진행한다. 모든 message는 다른 message
함수에 전달하기 전에 초기화해야 하고, 초기화된 message는 정확히 한 번
zlink_msg_close로 닫아야 한다. 닫은 뒤 zlink_msg_t 구조체는
유효하지 않으며 재사용하기 전에 다시 초기화해야 한다.
초기화 방법은 세 가지다.
- 빈 message —
zlink_msg_init이 길이 0의 빈 message로 초기화한다. - 크기 지정 —
zlink_msg_init_size가 지정한 크기의 내부 buffer를 할당한다. buffer 내용은 초기화되지 않으므로,zlink_msg_data로 pointer를 얻어 송신 전에 data를 채운다. - zero-copy —
zlink_msg_init_data가 caller가 제공한 buffer를 복사하지 않고 참조한다. buffer를 참조하는 마지막 message가 닫힐 때(송신된 message는 library가 전송을 마친 뒤 닫는다) library가 caller가 넘긴 release callbackffn_(data_, hint_)를 호출한다.
zero-copy message에서 buffer의 소유권은 callback이 경계다. caller는 callback이 호출될 때까지
buffer를 수정하거나 해제해서는 안 되며, buffer를 해제하는 일은 callback 안에서 한다 —
callback 반환 뒤 caller가 같은 buffer를 다시 해제하면 이중 해제다. ffn_이 NULL이면
library는 아무 callback도 호출하지 않으므로 buffer 수명은 전적으로 caller가 관리한다.
%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
participant App as Application
participant Lib as zlink library
App->>Lib: zlink_msg_init_data(msg, data, size, ffn, hint)
Note over Lib: data를 복사하지 않고 참조만 보관
App->>Lib: socket으로 송신 또는 zlink_msg_close(msg)
Note over App: ffn 호출 전까지 data 수정·해제 금지
Lib-->>App: ffn(data, hint) 호출
Note over App: ffn 안에서 buffer를 해제한다<br/>ffn 반환 뒤 다시 해제하지 않는다
payload에는 zlink_msg_data와 zlink_msg_size로
접근한다. zlink_msg_data가 반환한 pointer는 message가 닫히거나, 이동되거나, 송신될 때까지
유효하다.
3. Ownership 이동과 공유¶
message 내용의 소유권은 세 함수로 옮기거나 공유한다.
| 함수 | 용도 | 성공 후 상태 |
|---|---|---|
zlink_msg_move |
내용 이동 | src_는 빈 message가 되고 dest_가 원래 내용을 가진다. |
zlink_msg_copy |
경량 복사 | large/zero-copy storage는 두 message가 buffer를 공유하고, 작은 inline message(zlink_msg_init_size로 만든 size_ <= 29 byte payload)는 값으로 복사된다. |
zlink_msg_adopt |
binding이 초기화되지 않은 storage로 소유권 인수 | dest_가 초기화되어 원래 내용을 소유하고 src_는 빈 초기화 상태가 된다. |
large/zero-copy storage를 복사하면 두 message가 같은 data buffer를 공유한다. 같은 data
buffer를 공유하는 message 핸들의 수를 reference count(refcount)라 하며, refcount가 0이 되면
buffer를 해제한다. zlink_msg_copy()는 count를 atomic으로 증가시키고 zlink_msg_close()는
atomic으로 감소시키므로, 같은 storage를 공유하는 서로 다른 zlink_msg_t 핸들을 서로 다른
thread에서 복사하거나 닫는 것은 안전하다. 현재 count는
zlink_msg_refcnt로 조회한다.
thread 규칙은 핸들 단위다. 하나의 zlink_msg_t instance를 여러 thread에서 동시에 접근하면
안 된다. 동시 접근이 필요하면 zlink_msg_copy()로 별도 핸들을 만들어야 한다.
4. Multipart¶
이 절은 multipart를 지원하는 socket에 적용한다. STREAM은 part 하나를 보내는 송신과 RAW/PACKET 수신을 사용한다.
여러 frame(part)을 하나의 논리적 message로 묶어 전송하는 방식을 multipart라 한다. 송신 API는
연속된 zlink_msg_t 배열과 part 수를 한 번에 받아 record 하나로 원자적으로 제출한다. 다른
sender의 part가 그 record 안에 섞이지 않는다.
zlink_msg_t 구조체의 연속 배열로 저장한 multipart message는
zlink_multipart_close로 모든 part를 한 번에 닫는다. whole-message
수신(zlink_recv·zlink_router_recv)이
그런 배열을 채우는 생성 경로다: caller가 zlink_msg_t 배열과 capacity를 주면 Core가 record의 모든
part를 앞에서부터 채우고 *part_count_out_에 개수를 쓴다. 성공 시 각 슬롯은 caller-소유 part이며(호출
전 초기화 불필요), caller는 zlink_multipart_close(parts, count)로 정확히 한 번 닫는다. capacity가
record의 part 수보다 작으면 record를 소비하지 않고 필요한 개수만 *part_count_out_에 쓴 뒤
ZLINK_RECV_BUFFER_TOO_SMALL(errno == ENOBUFS)을 반환하므로, 부분 소비로 절반짜리 record 상태가
남지 않는다.
multipart와 thread의 관계는 다음과 같다. PAIR·DEALER·ROUTER에서는 여러 thread가 같은 socket에 각자 독립된 multipart 배열을 동시에 제출할 수 있다. 한 호출에 전달한 배열과 각 슬롯은 호출이 끝날 때까지 다른 thread가 접근하면 안 된다. receive는 single-consumer 계약을 따른다.
5. 타입과 상수¶
zlink_msg_t¶
typedef struct zlink_msg_t
{
unsigned char _[64]; // 불투명 storage (64 byte). 직접 접근하지 않는다
} zlink_msg_t;
zlink_msg_t는 64 byte 불투명 message 구조체다. 내부 layout은 플랫폼에 따라 다르며 직접
접근해서는 안 된다. 공개 header는 플랫폼별 alignment(예: 64-bit에서 8 byte)를 함께
선언한다. 모든 message는 사용 전에 초기화하고 사용 후에 닫아야 한다(§2).
zlink_routing_id_t¶
typedef struct zlink_routing_id_t
{
uint8_t size; // data에서 유효한 byte 수
uint8_t data[255]; // routing ID byte 열 (최대 255 byte)
} zlink_routing_id_t;
ROUTER socket이 특정 peer를 식별해 주소를 지정하는 데 사용하는 고유 byte 열을 routing
ID라 한다. zlink_routing_id_t는 이 routing ID를 전달하며, size는 data에서 유효한
byte 수를 나타낸다.
zlink_free_fn¶
zlink_free_fn은 zero-copy message 생성을 위해 zlink_msg_init_data()에서 사용되는
callback 타입이다. message data buffer가 더 이상 필요하지 않을 때 library가 이 함수를
호출한다.
6. 함수¶
모든 zlink_msg_* 함수에 공통인 입력 규칙: handle이 NULL이면 errno == EFAULT를 설정한다.
zlink_msg_close, zlink_msg_data, zlink_msg_size, zlink_msg_refcnt와 zlink_msg_adopt의
src_는 초기화된 message여야 하며, 유효하지 않으면(미초기화·이미 close) 역시 EFAULT다.
zlink_msg_init*는 초기화되지 않은 non-NULL storage를 받으며 그 storage의 이전 상태는
검사하지 않는다 — 이미 초기화된 message에 다시 init하면 이전 내용이 해제되지 않으므로 먼저
close한다. zlink_msg_move와 zlink_msg_copy는 두 pointer가 non-NULL이면 진행하며 src_는
초기화된 message여야 한다. 이때 각 함수가 반환하는 값은 다음과 같다.
| 함수 | 반환값 |
|---|---|
zlink_config_result_t를 반환하는 함수 |
ZLINK_CONFIG_INVALID_HANDLE |
zlink_msg_data |
NULL |
zlink_msg_size |
0 |
zlink_msg_refcnt |
-1 |
zlink_msg_init¶
빈 message를 초기화한다.
msg_를 길이 0의 빈 message로 초기화한다. message는 최종적으로 zlink_msg_close()로
해제해야 한다. zlink_msg_t를 다른 message 함수에 전달하기 전에 항상 초기화한다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다. 각 zlink_msg_t는 한 번에 하나의 thread에서만
사용해야 한다.
참고: zlink_msg_init_size, zlink_msg_init_data, zlink_msg_close
zlink_msg_init_size¶
지정한 크기의 message를 초기화한다.
size_ byte의 내부 buffer를 할당하고 msg_를 초기화한다. buffer 내용은 초기화되지
않는다. zlink_msg_data()로 buffer pointer를 얻어 송신 전에 data를 채운다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
에러:
- ENOMEM -- 할당 실패.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_data, zlink_msg_size
zlink_msg_init_data¶
외부 data buffer로 message를 초기화한다 (zero-copy).
ZLINK_EXPORT zlink_config_result_t zlink_msg_init_data (
zlink_msg_t *msg_, void *data_, size_t size_, zlink_free_fn *ffn_, void *hint_);
caller가 제공한 size_ byte의 buffer data_를 복사하지 않고 참조하는 message를 생성한다.
library가 buffer를 더 이상 필요로 하지 않을 때(message가 송신되거나 닫힌 후) caller가
buffer를 해제할 수 있도록 data_와 hint_를 인수로 callback ffn_을 호출한다. ffn_이
NULL이면 callback이 호출되지 않으며, caller는 buffer가 message보다 오래 존재하도록
보장해야 한다.
이 함수는 진정한 zero-copy message 전달을 가능하게 한다. caller는 ffn_이 호출될 때까지
data_를 수정하거나 해제해서는 안 된다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다.
참고: zlink_free_fn, zlink_msg_data
zlink_msg_close¶
message 자원을 해제한다.
message와 관련된 모든 자원을 해제한다. 초기화된 모든 message는 정확히 한 번 닫아야
한다. 닫은 후 zlink_msg_t 구조체는 유효하지 않으며 재사용하기 전에 다시 초기화해야
한다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_init, zlink_multipart_close
zlink_msg_move¶
source에서 대상으로 message 내용을 이동한다.
두 포인터가 같은 유효 message를 가리키는 경우(dest_ == src_, non-NULL), 내용을 변경하지 않고 ZLINK_CONFIG_INVALID_ARGUMENT와 EINVAL로
거부한다.
src_의 내용을 dest_로 이동한다. 성공한 이동 후 src_는 빈 message가 되고(새로
초기화된 message와 동일) dest_는 원래 내용을 포함한다. dest_의 이전 내용은 해제된다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_copy
zlink_msg_copy¶
message를 복사한다.
두 포인터가 같은 유효 message를 가리키는 경우(dest_ == src_, non-NULL), 내용을 변경하지 않고 ZLINK_CONFIG_INVALID_ARGUMENT와 EINVAL로
거부한다.
src_의 내용을 dest_로 복사한다. large/zero-copy storage는 두 message가 reference
counting으로 기본 data buffer를 공유하고, 작은 inline message는 값으로 복사된다. dest_의
이전 내용은 해제된다. 복사는 경량이며 큰 data payload를 복제하지 않는다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_move, zlink_msg_adopt
zlink_msg_adopt¶
별도의 init+move 단계 없이 source message의 소유권을 인수한다.
두 포인터가 같은 유효 message를 가리키는 경우(dest_ == src_, non-NULL), 내용을 변경하지 않고 ZLINK_CONFIG_INVALID_ARGUMENT와 EINVAL로
거부한다.
이미 dest_에 대한 storage를 보유하고 있고, 새로 수신한 native message의 소유권을
효율적으로 가져와야 하는 binding을 위한 함수다. zlink_msg_move와 달리 dest_는 현재
초기화된 message를 소유하지 않아야 한다 — 이미 초기화된 dest_에 zlink_msg_adopt를
호출하면 정의되지 않은 동작이 발생한다.
성공 시 dest_는 초기화된 message가 되어 src_의 원래 내용을 소유하고, src_는
payload를 소유하지 않는 빈 초기화 상태가 된다. 두 message 객체는 각각의 수명이 끝나기
전에 정확히 한 번 zlink_msg_close()해야 한다. 빈 src_를 close해도 인수한 payload에는
영향을 주지 않으며, close하지 않은 채 storage를 폐기하거나 다시 init하면 안 된다. 성공한
adopt 뒤 src_ storage를 재사용하려면 먼저 close한 다음 다시 init한다. 실패하면 src_가
원래 payload를 계속 소유하고 dest_는 초기화되지 않은 상태로 유지된다.
반환값: 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값. zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_move, zlink_msg_copy
zlink_msg_data¶
message data buffer에 대한 pointer를 반환한다.
message의 원시 data payload에 대한 pointer를 반환한다. pointer는 message가 닫히거나,
이동되거나, 송신될 때까지 유효하다. message가 초기화되지 않은 경우 NULL을 반환한다.
반환값: message data buffer에 대한 pointer.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_size
zlink_msg_size¶
message data 크기를 byte 단위로 반환한다.
message payload의 크기를 byte 단위로 반환한다. 빈 message의 경우 0을 반환한다.
반환값: byte 단위 크기.
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_data
zlink_msg_refcnt¶
message storage의 reference count를 반환한다.
reference-counted large/zero-copy storage면 현재 internal reference count를 반환한다.
inline storage나 borrowed constant storage처럼 internal reference counting 대상이 아닌
message 종류는 1을 반환한다. 실패 시 *error_out_에 설정 결과(zlink_config_result_t)가
기록되고, 성공 시 reference count가 기본 반환값으로 반환된다. error_out_은 선택
사항이다 — NULL을 전달하면 결과 코드 기록 없이 count 또는 -1 반환과 errno 설정만
관찰된다.
내부 reference count는 atomic 연산으로 관리된다. zlink_msg_copy()는 count를 atomic으로
증가시키고, zlink_msg_close()는 atomic으로 감소시킨다. 따라서 같은 underlying storage를
공유하는 서로 다른 zlink_msg_t 핸들을 서로 다른 thread에서 복사하거나 닫는 것은
안전하다.
zlink_msg_refcnt()는 counter의 atomic read를 수행한다. 반환값은 시점 snapshot이며,
호출자가 값을 확인하는 시점에 다른 thread가 copy/close로 이미 값을 변경했을 수 있다.
따라서 이 함수는 진단이나 assertion 용도에 적합하며, 제어 판단에는 적합하지 않다.
하나의 zlink_msg_t instance를 여러 thread에서 동시에 접근하면 안 된다. 동시 접근이
필요하면 zlink_msg_copy()로 별도 핸들을 만들어야 한다.
반환값: 현재 storage reference count. internal reference counting 대상이 아니면 1.
실패 시 -1을 반환하며 *error_out_에 zlink_config_result_t가 기록된다.
zlink_errno()는 진단용 내부 errno를 그대로 유지한다.
스레드 안전성: underlying reference count는 atomic이다. 같은 storage를 공유하는
서로 다른 zlink_msg_t 핸들이 다른 thread에서 copy/close되는 동안 이 함수를 호출하는
것은 안전하다. 단, 같은 zlink_msg_t instance에 대해 이 함수와 다른 zlink_msg_*
함수를 여러 thread에서 동시에 호출하는 것은 안전하지 않다.
참고: zlink_msg_copy, zlink_msg_close
zlink_multipart_close¶
multipart message 배열의 모든 part를 닫는다.
parts 배열의 각 요소에 대해 zlink_msg_close()를 호출하는 편의 함수다. zlink_msg_t
구조체의 연속 배열로 저장된 multipart message를 수신하거나 구성한 후 정리하는 데
사용한다.
반환값: 없음 (void).
스레드 안전성: thread-safe하지 않다.
참고: zlink_msg_close
7. 내부 불변 조건¶
결과 — 내부 send·queue·receive 경로는 한 번 제출된 multipart 배열의 record 경계와 metadata를 함께 보존한다. 공개 계약은 Multipart와 검증 요구가 소유한다.
Send¶
송신 경로는 한 호출의 parts_ 배열 전체를 record 하나로 admission한다. 성공·실패 모두 모든 입력
슬롯을 소비해 빈 initialized 상태로 두며, 실패하면 어떤 part도 peer에 보이지 않는다. 재시도는
호출 전에 보관한 record 전체로 한다. 상세 계약은
Socket 공통이 소유한다.
Receive¶
typed receive API는 완전한 record의 모든 part를 caller 배열에 한 번에 반환한다. Capacity가 부족하면 record를 소비하지 않고 필요한 수만 반환하므로 부분 수신 상태가 없다. 성공 시 record에 속한 source와 reply token 같은 output도 한 번에 정해진다.
Request/reply¶
Request·reply kind는 record의 내부 metadata로 함께 이동한다. Pipe와 queue는 이를 보존하고, typed receive 경로는 reply에 필요한 local token과 routing context를 message 밖의 별도 output으로 옮긴 뒤 public message에서 metadata를 제거한다. Application payload 앞에 request-reply protocol part를 추가하지 않는다.
8. 구현 및 contract test 검증 요구¶
공개 표면(zlink_msg_*·zlink_multipart_close 함수, 반환값·errno, zlink_free_fn callback
호출)만으로 다음을 확인한다. 각 항목은 unit test 하나로 이어진다.
초기화와 해제
- zlink_msg_init으로 초기화한 message는 길이 0이다 — zlink_msg_size가 0을 반환한다.
- zlink_msg_init_size가 성공하면 zlink_msg_size가 지정한 크기를 반환하고, 할당에 실패하면 ENOMEM이다.
- 초기화되지 않은 message에 zlink_msg_data를 호출하면 NULL을 반환한다.
- 초기화된 message는 정확히 한 번 zlink_msg_close로 닫고, 닫은 storage는 다시 초기화한 뒤에만 재사용할 수 있다.
zero-copy와 free callback
- zlink_msg_init_data로 만든 message가 송신되거나 닫힌 후, library가 data_와 hint_를 인수로 ffn_을 호출한다.
- ffn_이 NULL이면 callback을 호출하지 않는다.
이동·복사·adopt
- zlink_msg_move 성공 후 src_는 새로 초기화된 message와 동일한 빈 message이고, dest_가 원래 내용을 가진다.
- large/zero-copy storage를 zlink_msg_copy하면 payload를 복제하지 않고 buffer를 공유한다 — copy 후 zlink_msg_refcnt 반환값이 증가하고, 공유 핸들 하나를 close하면 다시 감소한다.
- inline storage나 borrowed constant storage message의 zlink_msg_refcnt는 1을 반환한다.
- zlink_msg_adopt 성공 후 dest_가 src_의 원래 내용을 소유하고 src_는 payload가 없는 빈 초기화 상태다. 빈 src_를 close해도 인수한 payload에는 영향이 없다.
- zlink_msg_adopt 실패 시 src_가 원래 payload를 계속 소유하고 dest_는 초기화되지 않은 상태로 남는다.
refcount와 thread
- 같은 storage를 공유하는 서로 다른 zlink_msg_t 핸들을 서로 다른 thread에서 copy·close해도 안전하며, buffer는 refcount가 0이 될 때 해제된다.
- zlink_msg_refcnt 실패 시 -1을 반환하고 *error_out_에 zlink_config_result_t가 기록된다.
multipart
- zlink_multipart_close는 배열의 각 요소에 zlink_msg_close를 호출한 것과 같은 결과를 남긴다.
- multipart send는 성공·실패 모두 모든 입력 슬롯을 소비해 빈 initialized 상태로 두고, 실패하면
peer에 어떤 part도 보이지 않는다.
- receive는 완전한 multipart record의 모든 part를 한 번에 반환하며 다른 sender의 part가 섞이지 않는다.
- receive capacity가 부족하면 필요한 part 수와 ZLINK_RECV_BUFFER_TOO_SMALL+ENOBUFS를 반환하고
record를 소비하지 않는다.
공통 반환 규약
- zlink_config_result_t를 반환하는 각 zlink_msg_* 함수는 성공 시 ZLINK_CONFIG_OK, 실패 시 zlink_config_result_t 값을 반환하며 zlink_errno()는 진단용 내부 errno를 그대로 유지한다.