가이드 목록 | 이전: Java | 다음: Python
Node.js 바인딩 가이드 (@zlink-systems/zlink)¶
이 장의 계약 소유 문서 — Node.js bindings 스펙이 다룬다. 이 장은 그 계약을 실제 샘플 코드로 보여준다.
Node.js에서 zlink를 쓰는 방법을 실제 샘플 코드 중심으로 설명합니다. 메시징 개념은 코어 가이드를 참고하세요.
설치¶
- Node.js 22 이상.
- 네이티브 코어가 플랫폼별 prebuild로 번들됩니다.
const zlink = require('@zlink-systems/zlink');
// 또는 ESM / TypeScript
import * as zlink from '@zlink-systems/zlink';
5분 예제¶
const zlink = require('@zlink-systems/zlink');
// 서버
const ctx = zlink.createContext();
const server = zlink.createPairSocket(ctx);
server.bind('tcp://127.0.0.1:5555');
const received = new zlink.Received();
server.recv(received);
console.log(received.parts[0].data().toString()); // PING
received.close();
await server.send().message(Buffer.from('ACK')).submit().admitted;
server.close();
ctx.close();
// 클라이언트
const ctx = zlink.createContext();
const client = zlink.createPairSocket(ctx);
client.connect('tcp://127.0.0.1:5555');
await client.send().message(Buffer.from('PING')).submit().admitted;
const received = new zlink.Received();
client.recv(received);
console.log(received.parts[0].data().toString()); // ACK
received.close();
client.close();
ctx.close();
핵심 타입¶
컨텍스트¶
메시지¶
Node 바인딩은 Buffer를 메시지로 직접 씁니다. message()를 호출하면 복사본을
만들기 때문에 원본 Buffer를 마음껏 재사용할 수 있습니다.
await socket.send().message(Buffer.from('hello')).submit().admitted;
await socket.send().message(Buffer.from([0x01, 0x02])).submit().admitted;
// 수신 후 페이로드 접근
const received = new zlink.Received();
socket.recv(received);
const data = received.parts[0].data(); // Buffer
const text = data.toString('utf8');
received.close();
HWM 대기 가능 send는 비동기 submit()과 동기 submit_sync() terminal을
제공합니다. Node 이벤트 루프에서는 결과 객체를 돌려주는 submit()을 기본으로 사용합니다.
submit()은 SendSubmission(result: OK|BACKPRESSURED 동기 필드, admitted: Promise<void>)을
돌려주며 DONTWAIT을 사용하고 admission은 socket completion queue에서 settle됩니다.
submit_sync()은 local admission까지 Core 안에서 blocking합니다.
const send = socket.send().message(Buffer.from('data')).submit(); // 결과 객체
if (send.result === SubmitResult.BACKPRESSURED) await send.admitted; // HWM일 때만 대기
socket.send().message(Buffer.from('data')).submit_sync(); // 동기 Core admission
Request는 reply까지 blocking하는 submit_sync()과, RequestSubmission(result·admitted에
reply: Promise<Message[]> 추가)을 돌려주는 submit()을 제공합니다. result가 OK면 바로
reply를 기다리면 되고, reply는 terminal 결과이며 별도 DATA receive가 아닙니다.
Core가 pre-admission operation을 접수한 뒤 retry를 소유하므로 caller retry queue를 만들거나
payload를 재전송하지 않습니다. 공용 native ZLINK_OPT_PENDING_MAX_MSGS/BYTES 제한은 pending
SEND와 REQUEST에 함께 적용되고 send 전용 pending 이름은 없습니다. Completion은 local
admission일 뿐 peer delivery나 application acknowledgement가 아닙니다.
Submit 전 cancellation은 호출하지 않는 것으로 처리합니다. Successful submit 뒤에는 public
Core cancel이 없으며 Promise 관찰을 버려도 socket owner가 늦은 completion을 drain합니다.
Bind/connect 전에 stream.options.recvMode를 zlink.StreamRecvMode.Raw 또는 .Packet으로
정한 뒤 각각 recv 또는 recvPacket을 사용합니다.
public poller가 socket의 zlink.PollEventFlag.PollCompletion owner이면 blocking request나
Promise가 남아 있는 동안 다른 thread가 wait() loop를 계속 실행해야 합니다. wait()가
native completion을 drain해 Node state를 settle/cleanup하므로 같은 thread에서 wait 사이에
blocking terminal을 호출하면 진행이 멈출 수 있습니다.
Received — 수신 봉투¶
const received = new zlink.Received();
socket.recv(received); // 동기 블로킹
try {
const parts = received.parts; // Message[]
const rid = received.routingId; // RoutingId 또는 null
const token = received.replyToken; // ROUTER request의 ReplyToken 또는 null
} finally {
received.close();
}
라우팅 ID¶
소유권과 수명¶
| 상황 | 규칙 |
|---|---|
submit() 성공 |
전달된 Buffer는 내부 복사되므로 원본 재사용 가능 |
recv() 성공 |
received.close() 필수 (finally 블록 권장) |
submit()(Promise) 완료 |
회신 파트 배열을 각각 part.close() |
ctx.close() |
하위 소켓의 블로킹 작업 중단 |
const received = new zlink.Received();
socket.recv(received);
try {
// 파트 처리
} finally {
received.close();
}
공유·이전·복제 (copy / move / clone)¶
Message payload를 다루는 세 가지 명시적 동작입니다. 이름과 의미는 모든 바인딩에서
동일하며 Core C API(zlink_msg_copy/zlink_msg_move)와 1:1로 대응합니다.
| 동작 | 시그니처 | 의미 | 언제 |
|---|---|---|---|
copy() |
copy(): Message |
ref-count 공유 — 같은 버퍼를 가리키는 새 Message, 원본 유효 유지 |
같은 payload를 보관하며 원본도 계속 써야 할 때 |
move(dest) |
move(dest: Message): void |
소유권 이전 — dest로 넘기고 호출자는 empty |
받은 메시지를 사본 없이 그대로 다시 보낼 때(relay/echo) |
clone() |
clone(): Message |
깊은 복사 — 독립 버퍼 | 복제 후 payload를 독립적으로 수정할 때 |
// Copy: 같은 버퍼를 공유하는 새 핸들. 둘 다 각자 close.
const shared = msg.copy();
await socket.send().message(shared).submit().admitted; // shared는 소비됨
// msg는 여전히 유효
// Move: 받은 메시지를 사본 없이 그대로 echo (가장 효율적)
const out = new zlink.Message();
receivedPart.move(out); // receivedPart는 empty가 됨
await socket.send(routingId).message(out).submit().admitted;
// Clone: 독립 복제 후 수정
const dup = msg.clone();
⚠ Breaking change: 기존
copy()는 깊은 복사였으나 이제copy()는 ref-count 공유이고, 깊은 복사는clone()으로 이동했습니다. JS는 동일 시그니처를 반환 의미만 달리해 공존시킬 수 없어 alias가 불가능하므로 major 버전 breaking으로 처리합니다. 기존 코드의copy()(깊은 복사 의도)는 반드시clone()으로 바꾸세요.참고(refcount 타이밍): Node는 payload를
Buffer로 노출합니다. 노출된Buffer가 살아 있는 동안 native 저장소는 그Buffer가 GC될 때 정리됩니다. 그래서copy()로 공유한 뒤 한쪽을close()해도refCount()값은 즉시 1로 떨어지지 않고 버퍼 GC 후 반영됩니다(진단용 표시일 뿐, 동작·안전·소유권 독립엔 영향 없음).
에러 처리¶
Node 바인딩은 작업별 에러 클래스를 던집니다.
try {
await socket.send().message(Buffer.from('data')).submit().admitted;
} catch (error) {
if (error instanceof zlink.SubmitError) {
if (error.result === zlink.SubmitResult.Backpressured) {
// 재시도
} else {
throw error;
}
}
}
에러 클래스는 SubmitError, RequestError, RecvError, BindError,
ConnectError, ConfigError, CloseError, HandlerError입니다.
각각 .result 속성으로 결과 코드를 노출합니다.
C API 대응표¶
| C API | Node API |
|---|---|
zlink_ctx_new() |
zlink.createContext() |
zlink_ctx_term() |
ctx.close() |
zlink_socket(ctx, type) |
zlink.createPairSocket(ctx) 등 |
zlink_bind(s, ep) |
socket.bind(ep) |
zlink_connect(s, ep) |
socket.connect(ep) |
zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) + NONE |
socket.send().message(buf).submit_sync() |
| DONTWAIT send + completion pull | await socket.send().message(buf).submit().admitted |
zlink_recv(..., parts_out, capacity, count_out, ...) |
socket.recv(received) |
zlink_msg_data(msg) |
part.data() (Buffer) |
zlink_routing_id_t |
zlink.RoutingId |
zlink_socket_monitor_open(...) |
socket.monitorOpen([...]) |
zlink_poller_new() |
zlink.createPoller() |
zlink_timer_new() |
zlink.createTimer() |
네이티브 라이브러리 / 배포¶
네이티브 코어는 플랫폼별 prebuild로 패키지에 들어갑니다. 별도 빌드 없이
npm install만으로 동작합니다.
const [major, minor, patch] = zlink.version(); // [number, number, number]
console.log(`zlink ${major}.${minor}.${patch}`);
스레딩 유의사항. Node는 단일 스레드 이벤트 루프 모델입니다.
| 항목 | 규칙 |
|---|---|
Context·소켓 |
메인 이벤트 루프에서 사용 |
블로킹 recv() |
이벤트 루프를 막으므로 짧게 사용하거나 논블로킹 + 폴러 권장 |
동기 submit_sync() |
HWM 대기 시 이벤트 루프 전체를 멈춤 — 이벤트 루프에서 사용하지 않음 |
비동기 submit() |
Promise 기반 — 이벤트 루프를 막지 않음 |
Node에서는 blocking send를 이벤트 루프에서 실행하지 않습니다. 비동기 submit()을
await하고 submit_sync()은 적절한 worker thread에서만 사용합니다.
샘플¶
bindings/node/samples/ 디렉터리의 검증된 샘플입니다.
| 파일 | 설명 |
|---|---|
pair_recv_sample.ts |
PAIR 송수신 |
dealer_router_recv_sample.ts |
DEALER/ROUTER 송수신 |
request_reply_sample.ts |
요청/응답 |
pubsub_recv_sample.ts |
PUB/SUB 발행·구독 |
stream_recv_sample.ts |
STREAM 원시 TCP |
stream_packet_sample.ts |
STREAM PACKET pull |
monitor_recv_sample.ts |
모니터 이벤트 수신 |
SPOT·Actor 예제는 core 바인딩이 아니라 framework 샘플이 다룬다 — 아래 더 보기의 서비스 링크를 본다.
JavaScript¶
JavaScript는 별도 네이티브 바인딩 없이 Node 바인딩(@zlink-systems/zlink)을
그대로 씁니다. 위의 설치·핵심 타입·소유권·에러·대응표가 똑같이 적용되고
TypeScript 타입 표기만 빠집니다.
- 의존성:
@zlink-systems/zlink(위와 동일). TypeScript 빌드 단계가 필요 없고 순수.js로 바로require한다.
const zlink = require('@zlink-systems/zlink');
const ctx = zlink.createContext();
const socket = zlink.createPairSocket(ctx);
// ... 사용 후 socket.close(); ctx.close();
- 소유권: 명시적
close()로 정리한다(Node와 동일, GC에 의존하지 않는다). - 샘플:
bindings/javascript/samples/(.js)에 Node 샘플과 같은 canonical 세트가 있다. Node 바인딩을 빌드한 뒤node로 바로 실행한다.
cd bindings/node && npm run build # 공유 런타임 빌드
cd ../javascript/samples
node pair_recv_sample.js # 또는 ./run_samples.sh
코어 가이드의 언어 탭에는 JavaScript 칸이 따로 있어 메시징·서비스 사용법을 JavaScript 코드로 바로 볼 수 있다.
더 보기¶
소켓 패턴 - 소켓 패턴 개요 — PAIR · PUB/SUB · DEALER · ROUTER · STREAM · 프록시
서비스 - Framework 서비스 개요