콘텐츠로 이동

가이드 목록 | 이전: 개요 | 다음: C++

.NET 바인딩 가이드(Zlink package)

이 장의 계약 소유 문서.NET bindings 스펙이 다룬다. 이 장은 그 계약을 실제 샘플 코드로 보여준다.

.NET에서 Zlink package를 사용하는 방법을 설치·핵심 타입·소유권·에러 처리·배포까지 한 챕터로 정리합니다. 메시징 개념(소켓 패턴, 서비스, 운영)의 깊은 설명은 더 보기의 코어 가이드를 참고하세요.


설치

단일 NuGet package Zlink로 제공되며 native Core가 함께 포함됩니다.

dotnet add package Zlink
  • .NET 8.0 이상 (net8.0).
  • 네이티브 설치 불필요 — RID별 바이너리를 자동 로드합니다. (네이티브 라이브러리 참고)
using Systems.Zlink;   // 모든 공개 API는 이 네임스페이스에 있습니다

5분 예제

Pair 소켓으로 한쪽이 PING을 보내고 다른 쪽이 ACK로 답하는 최소 예제입니다. 서버는 bind, 클라이언트는 connect 합니다.

// 서버
using var ctx = Zlink.CreateContext();
using var server = ctx.CreatePairSocket();
using var mon = server.MonitorOpen(SocketEvent.ConnectionReady);
server.Bind("tcp://127.0.0.1:5555");
mon.Recv();   // 연결될 때까지 대기

using var received = Received.Create();
server.Recv(received);
Console.WriteLine(received.FirstPart().GetString());   // PING

using var reply = Message.From("ACK");
server.Send().Message(reply).Submit();
// 클라이언트
using var ctx = Zlink.CreateContext();
using var client = ctx.CreatePairSocket();
using var mon = client.MonitorOpen(SocketEvent.ConnectionReady);
client.Connect("tcp://127.0.0.1:5555");
mon.Recv();

using var ping = Message.From("PING");
client.Send().Message(ping).Submit();

using var received = Received.Create();
client.Recv(received);
Console.WriteLine(received.FirstPart().GetString());   // ACK

핵심 타입

모든 기능이 공유하는 4가지 기본 타입입니다.

1. 컨텍스트 (Context)

프로세스의 런타임 진입점입니다. 보통 하나만 만들고 모든 소켓·서비스를 여기서 생성합니다.

using var ctx = Zlink.CreateContext();
ctx.Options.IoThreads  = 4;     // I/O 스레드 수
ctx.Options.MaxSockets = 1024;  // 최대 소켓 수
// 옵션은 소켓을 만들기 전에 설정하세요.

IContextIDisposable/IAsyncDisposable입니다. 종료 시 Shutdown()으로 진행 중인 작업을 멈출 수 있고 using으로 자동 해제됩니다.

2. 메시지 (Message)

하나의 페이로드 프레임입니다. 문자열·바이트·미리 할당 버퍼로 만들 수 있습니다.

byte[] buffer = GetPayload();

using var fromText  = Message.From("payload");      // 문자열(UTF-8)
using var fromBytes = Message.From(buffer);         // byte[] / ReadOnlySpan<byte> 복사
using var sized     = new Message(1024);            // 미리 할당 후 AsSpan()에 채움

int    size = fromText.Size;
string text = fromText.GetString();                  // UTF-8 디코딩
ReadOnlySpan<byte> view = fromText.AsReadOnlySpan();  // 복사 없이 읽기
byte[] copy             = fromText.ToArray();         // 복사해서 꺼내기

Message는 네이티브 저장소를 소유하므로 IDisposable입니다. AsSpan() / AsReadOnlySpan()이 주는 span은 메시지가 살아있는 동안만 유효합니다. 메시지 모델 개념은 메시지 API를 참고하세요.

바인딩은 JSON, Protobuf, MessagePack 같은 객체 codec package를 제공하지 않는다. 이 계층은 raw Message와 byte payload를 주고받는 저수준 API만 유지한다. 객체 직렬화가 필요하면 framework codec extension을 framework 구성 단계에 등록한다. framework의 actor join callback처럼 raw Message를 직접 주고받는 표면에서는 application 계층에서 명시적으로 byte payload를 만들고 해석한다.

HWM 대기 가능 send는 동기 Submit()과 비동기 Async()를 제공합니다. Submit()은 local HWM admission까지 Core 안에서 blocking합니다. 비동기 실행 흐름에서는 await ...Async()를 사용하며, DONTWAIT submit 뒤 socket completion queue에서 settle됩니다.

socket.Send().Message(message).Submit();       // 동기 Core admission
await socket.Send().Message(message).Async();  // 비동기 완료

Request는 reply까지 blocking하는 Submit()과 socket completion queue에서 settle되는 Task<IReadOnlyList<Message>>를 반환하는 Async()를 제공합니다. 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가 아닙니다.

CancellationToken은 pre-submit 호출을 막거나 managed waiter의 대기를 중단할 수 있습니다. Core가 payload를 접수한 뒤에는 cancellation이 Core admission이나 request를 취소하지 않으며, socket owner가 늦은 completion도 drain하고 해제합니다. Bind/connect 전에 stream.Options.ReceiveModeStreamReceiveMode.Raw 또는 .Packet으로 정하고 각각 Recv 또는 RecvPacket을 사용합니다.

public poller가 socket의 PollEventFlags.PollCompletion owner이면 blocking request나 Task가 남아 있는 동안 다른 thread가 Wait() loop를 계속 실행해야 합니다. Wait()가 native completion을 drain해 managed state를 settle/cleanup하므로 같은 thread에서 wait 사이에 blocking terminal을 호출하면 completion이 멈출 수 있습니다.

3. 수신 (Received)

수신 결과를 담는 재사용 가능한 봉투입니다. 핫 패스에서 한 번 만들어 Recv(...) 루프에서 재사용하면 할당이 사라집니다.

using var received = Received.Create();
socket.Recv(received);

Message      first = received.FirstPart();   // 첫 파트(소유권 이전 없음)
string       body  = first.GetString();
RoutingId?   from  = received.RoutingId;     // 라우팅 경로가 있으면
ReplyToken?  token = received.ReplyToken;    // ROUTER request이면
IReadOnlyList<Message> parts = received.Parts;  // 멀티파트 전체

4. 라우팅 ID (RoutingId)

피어·스팟·액터를 식별하는 바이너리 안전 값 타입입니다. 정적 팩토리로만 만듭니다. 개념과 정책은 라우팅 ID를 참고하세요.

RoutingId a = RoutingId.From("order-client");       // UTF-8 문자열
RoutingId b = RoutingId.From(0xC0FFEEu);             // uint32(빅엔디안)
RoutingId c = RoutingId.From(Guid.NewGuid());        // 16바이트 UUID
RoutingId d = RoutingId.FromHex("0a1b2c");           // 원시 hex
string    s = a.ToString();                          // 표시용 문자열
string    h = a.ToHex();                             // 원시 바이트 보존용

소유권과 수명

IContext·소켓·Message·Received는 모두 네이티브 리소스를 감싸며 IDisposable(및 대부분 IAsyncDisposable)을 구현합니다. 만든 것은 반드시 해제하세요 — 항상 using(또는 await using).

  • 소켓은 그것을 만든 컨텍스트보다 먼저 dispose 하세요.
  • Request().Async()·Join(...).Async()가 반환하는 응답 파트 (IReadOnlyList<Message>)는 호출자 소유입니다 — 사용 후 dispose 하세요.
  • span을 보관하려면 ToArray()/AsReadOnlyMemory()로 복사하세요.

공유·이전·복제 (Copy / Move / Clone)

Message payload를 다루는 세 가지 명시적 동작입니다. 이름과 의미는 모든 바인딩에서 동일하며 Core C API(zlink_msg_copy/zlink_msg_move)와 1:1로 대응합니다.

동작 시그니처 의미 언제
Copy() Message Copy() ref-count 공유 — 같은 버퍼를 가리키는 새 Message, 원본 유효 유지 같은 payload를 보관하며 원본도 계속 써야 할 때
Move(dest) void Move(Message dest) 소유권 이전dest로 넘기고 호출자는 empty 받은 메시지를 사본 없이 그대로 다시 보낼 때(relay/echo)
Clone() Message Clone() 깊은 복사 — 독립 버퍼 복제 후 payload를 독립적으로 수정할 때
// Copy: 같은 버퍼를 공유하는 새 핸들. 둘 다 각자 Dispose.
using Message shared = msg.Copy();
socket.Send().Message(shared).Submit();   // shared는 소비됨
// msg는 여전히 유효

// Move: 받은 메시지를 사본 없이 그대로 echo (가장 효율적)
var outMsg = new Message();
receivedPart.Move(outMsg);                 // receivedPart는 empty가 됨
socket.Send(routingId).Message(outMsg).Submit();

// Clone: 독립 복제 후 수정
using Message dup = msg.Clone();

Copy()는 ref-share이므로 mutation 격리를 보장하지 않습니다 — 독립 수정이 필요하면 Clone()을 쓰세요. .NET의 CopyTo(Span<byte>)/CopyTo(IBufferWriter<byte>)는 payload를 버퍼에 채우는 span-fill로 Clone(Message deep copy)과 별개이며 그대로 유지됩니다.

스레드 안전성 규칙은 스레드 안전성을 참고하세요. IContext는 여러 스레드에서 공유해도 안전합니다. 소켓은 안전하지 않습니다 — 같은 소켓을 둘 이상의 스레드에서 동시에 호출하지 마세요.


에러 처리

하드 실패는 작업별 타입 예외로 나타납니다. 모두 ZlinkException을 상속하며 Code(정수 코드)와 작업별 Result(열거형)를 노출합니다.

try
{
    socket.Bind("tcp://127.0.0.1:5555");
}
catch (ZlinkBindException ex) when (ex.Result == ZlinkBindException.ErrorCode.AddrInUse)
{
    Console.Error.WriteLine("포트가 이미 사용 중입니다.");
}
catch (ZlinkException ex)
{
    Console.Error.WriteLine($"zlink 오류 {ex.Code}: {ex.Message}");
    throw;
}
예외 발생 작업
ZlinkSubmitException 송신/발행 (Submit)
ZlinkRequestException 요청/응답 (Request) — TimedOut
ZlinkRecvException 수신 (Recv)
ZlinkBindException / ZlinkConnectException 바인드/연결
ZlinkConfigException 옵션/설정
ZlinkCloseException / ZlinkHandlerException 종료/콜백

논블로킹 수신에서 데이터가 없으면 Recv(...)false를 반환합니다. 비동기 send는 Core DONTWAIT을 사용하고 실패를 Task로 전달합니다.

if (!socket.Recv(received, RecvFlags.DontWait)) { /* 데이터 없음 */ }
try { await socket.Send().Message(m).Async(); }
catch (ZlinkSubmitException ex) when (ex.Result == SubmitResult.Backpressured) { /* 백프레셔 */ }

C API 대응표

C 코어(zlink.h)에서 넘어오거나 다른 언어 바인딩과 비교할 때 쓰는 압축 매핑입니다. .NET은 raw 함수 대신 객체와 플루언트 빌더로 감싸므로 1:1은 아니지만 개념 단위로는 대응합니다. 전체 C 함수 목록은 코어 C API 가이드를 참고하세요.

영역 C API (zlink_*) .NET
컨텍스트 zlink_ctx_new / zlink_ctx_term Zlink.CreateContext() / IContext.Dispose()
컨텍스트 옵션 zlink_ctx_set / zlink_ctx_get IContext.Options (IoThreads, MaxSockets, …)
소켓 생성 zlink_socket(ctx, TYPE) ctx.Create<Type>Socket() (CreatePairSocket() 등)
바인드 / 연결 zlink_bind / zlink_connect socket.Bind(...) / socket.Connect(...)
연결 해제 zlink_disconnect / zlink_disconnect_rid socket.Disconnect(string) / socket.DisconnectRid(RoutingId)
소켓 옵션 zlink_set_option / zlink_get_option 소켓별 강타입 속성(socket.Options)
routing id zlink_set_routing_id / zlink_get_routing_id socket.SetRoutingId(RoutingId) / socket.GetRoutingId()
메시지 생성 zlink_msg_init / _init_size / _init_data new Message(size) / Message.From(...)
메시지 접근 zlink_msg_data / zlink_msg_size Message.AsReadOnlySpan() / Message.Size
메시지 해제 zlink_msg_close / zlink_multipart_close Message.Dispose() / Zlink.MultipartClose(parts)
동기 송신 zlink_send / zlink_send_rid (part 배열 + count, NONE) socket.Send().Message(...).Submit()
비동기 송신 DONTWAIT send + completion pull await socket.Send().Message(...).Async()
수신 zlink_recv (출력 배열 + capacity + count) socket.Recv(Received)
요청 / 응답 zlink_request / zlink_reply dealer.Request()....Async() / router.Reply(rid, token)
구독 zlink_set_subscription / zlink_subscribe socket.SetSubscription(...) / socket.Subscribe(TopicMessage)
모니터 zlink_socket_monitor_open / _recv socket.MonitorOpen(...) / monitor.Recv()
폴러 / 타이머 zlink_poller_* / zlink_timer_* Zlink.CreatePoller() / Zlink.CreateTimer()
프록시 zlink_proxy Zlink.Proxy(...)

이름 규칙: C의 snake_case는 .NET에서 PascalCase가 됩니다. C의 whole-message 배열과 count는 .NET에서 플루언트 빌더의 .Message(...) 누적으로 표현됩니다. Public 모양은 언어 관례를 따르되 의미 계약은 동일합니다.


네이티브 라이브러리 / 배포

Zlink는 native Core를 runtimes/<rid>/native 아래 포함하므로 일반 빌드에서는 추가 설정이 필요 없습니다. 환경변수 ZLINK_LIBRARY_PATH로 로드 경로를 지정할 수 있습니다. self-contained/single-file/Native AOT 게시 시에는 대상 RID 자산이 출력에 포함되는지 확인하세요 (dotnet publish -r <rid>).

스레딩: IContext는 스레드 안전하며 여러 스레드에서 공유 가능합니다. 소켓은 단일 스레드 소유 — 전체 규칙은 스레드 안전성 참고. Submit()은 HWM admission을 기다리는 동안 호출 thread를 멈춥니다. Plain thread에서는 그 thread만 대기하므로 사용할 수 있습니다. Thread를 점유하지 않고 완료를 기다려야 하면 Async()를 사용합니다.


샘플

bindings/dotnet/samples/에 기능별 실행 가능한 예제가 있습니다.

샘플 다루는 기능
PairRecv PAIR 송수신
DealerRouterRecv DEALER/ROUTER 라우팅
RequestReplyAsync 비동기 요청/응답
PubSubRecv PUB/SUB 토픽
MonitorRecv 소켓 모니터
StreamRecv, StreamPacketCallback STREAM RAW/PACKET pull(legacy sample directory 이름)

SPOT·Actor 예제는 core 바인딩이 아니라 framework 샘플이 다룬다 — Spot · Actor 가이드를 본다.

실행: ./samples/run_samples.sh (또는 run_samples.ps1).


더 보기

소켓 패턴 - 소켓 패턴 개요 - PAIR - PUB/SUB - DEALER - ROUTER - STREAM - 프록시

서비스 - Framework 서비스 개요 - Spot - Actor

운영 - 소켓 옵션 - TLS 보안 - 모니터링 - 스레드 안전성 - 메시지 API - 라우팅 ID