수정 이력(1건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22176779)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「Named Pipe 실무 ── Windows 프로세스 간 통신의 정석을 설계부터 보안까지」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-named-pipes-practical-guide/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22176779
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22176780
「상주 서비스에 설정 화면에서 명령을 보내고 싶다」「관리자 권한이 필요한 처리만 별도 프로세스로 나누고 싶다」── Windows에서 이런 프로세스 간 통신(IPC)을 만들 때 가장 먼저 검토하고 싶은 것이 Named Pipe입니다.
같은 PC 안의 통신에서 첫 번째 후보가 되는 이유는 읽고 쓰기가 손쉽다는 점만이 아닙니다. Windows의 ACL로 접속자를 좁히고, 필요하면 상대의 Windows 계정을 확인해 그 권한으로 처리할 수 있다는 점이 큰 이점입니다. 다만 파이프를 만들기만 해서 안전해지는 것은 아니고, 접속과 권한 설정이 필요합니다.123
이 글에서는 접속 형태 → 메시지 경계 → 서버 구성 → 보안 → 장애 시 동작 순서로 설계를 정리합니다. 대상은 Windows 업무 앱이나 서비스를 작성하는 개발자입니다. 프로세스 간 통신 판단표 글에서 제시한 「같은 머신 IPC의 첫 번째 후보」를 구현 시점의 판단까지 파고듭니다.
1. 먼저 결론: 설계에서 정할 것은 5가지
API를 고르기 전에 통신 상대, 데이터의 단위, 동시 접속, 권한, 실패 시 처리를 정해 둡니다.
| 정할 것 | 기본적인 선택 방법 | 자세히 읽기 |
|---|---|---|
| 어디와 통신할 것인가 | 같은 PC 안의 Windows 프로세스라면 Named Pipe를 첫 번째 후보로 삼는다. 원격 확장, 다른 OS, 기존 프로토콜 활용을 중시한다면 TCP 기반을 검토한다 | 2장 |
| 무엇을 한 건으로 볼 것인가 | 한 번의 쓰기를 한 건으로 다루려면 메시지 모드. 이미 프레이밍이 있다면 바이트 모드 | 3장 |
| 여러 클라이언트를 어떻게 받을 것인가 | 같은 이름의 인스턴스를 여러 개 준비한다. 새로 만드는 .NET 구현이라면 비동기 I/O와 async/await가 간단하다 | 4장 |
| 누구에게 접속과 권한 차용을 허용할 것인가 | 원격 거부, ACL, 첫 인스턴스의 보장, 클라이언트의 impersonation 수준을 한 세트로 설계한다 | 5, 6장 |
| 통신이 실패하면 어떻게 할 것인가 | 기동 대기, 재접속, 메시지 상한, 응답 확인을 프로토콜에 포함한다 | 7장 |
Named Pipe는 ACL에 의한 접속 제어와 클라이언트 impersonation을 Windows의 구조로 이용할 수 있습니다. localhost의 TCP를 쓰는 경우에는 통신 상대가 누구인지 확인하는 인증을 따로 설계해야 합니다. 한편 앞으로 원격 통신으로 넓히고 싶다, 다른 OS와도 통신하고 싶다, gRPC 같은 기존 자산을 쓰고 싶다면 TCP 기반이 유리합니다.23
특히 중요한 것은 impersonation에 실패한 요청을 실행하지 않는 것입니다.실패를 무시하면 클라이언트가 아니라 서버 자신의 권한으로 처리가 이어져 버립니다. 특권 서비스를 만든다면 코드 예제만이 아니라 5, 6장까지 확인해 주십시오.4
2. 접속 구조: 이름은 공통, 통신 경로는 클라이언트마다
2.1 생성, 접속, 읽고 쓰기의 역할을 나눈다
Named Pipe는 \\.\pipe\MyCompany.MyApp.Control 같은 이름으로 식별하는 단방향 또는 양방향 통신 경로입니다. 서버와 클라이언트는 처음 사용하는 API가 다릅니다.1
| 단계 | 서버 측 | 클라이언트 측 |
|---|---|---|
| 통신 경로를 준비한다 | CreateNamedPipe로 인스턴스를 만든다 |
서버가 준비한 파이프 이름을 쓴다 |
| 접속한다 | ConnectNamedPipe로 클라이언트를 기다린다 |
CreateFile로 같은 이름을 연다 |
| 데이터를 주고받는다 | ReadFile / WriteFile로 읽고 쓴다 |
ReadFile / WriteFile로 읽고 쓴다 |
접속 후에는 양쪽 모두 파일 I/O와 같은 형태로 다룰 수 있습니다. 이름의 지정뿐 아니라 다음에 나오는 인스턴스와 방향의 사고방식을 잡아 두면 다중 접속이나 접속 오류도 이해하기 쉬워집니다.
2.2 인스턴스 하나가 클라이언트 하나를 맡는다
같은 이름의 파이프를 여러 개 만들 수 있고, 인스턴스 하나가 클라이언트 하나와의 통신 경로가 됩니다.이름이 같다고 해서 모든 클라이언트가 하나의 통신 경로를 공유하는 것은 아닙니다. 최초의 CreateNamedPipe에서 최대 인스턴스 수를 지정합니다. 상한 지정에는 PIPE_UNLIMITED_INSTANCES도 준비되어 있습니다.15
flowchart TB
accTitle: Named Pipe의 기본 구조
accDescr: 서버는 같은 이름의 파이프 인스턴스를 여러 개 만들어 ConnectNamedPipe로 접속을 기다리고, 각 클라이언트는 CreateFile로 이름을 열어 하나의 인스턴스와 일대일 양방향 통신 경로를 가진다
s["서버"] --> i1["인스턴스 1"]
s --> i2["인스턴스 2"]
s --> i3["인스턴스 3"]
c1["클라이언트 A"] <--> i1
c2["클라이언트 B"] <--> i2
c3["클라이언트 C"] <--> i3
그림 1: 양방향 파이프의 예. 같은 이름의 인스턴스를 여러 개 준비하면 서버는 여러 클라이언트와 각각 일대일로 통신한다.
2.3 「방향」은 서버 측에서 본 이름
클라이언트의 접근 지정은 서버가 만든 파이프의 방향과 맞춥니다. 어긋나면 CreateFile이 실패합니다.6
| 서버가 만드는 방향 | 서버의 동작 | 클라이언트가 지정하는 접근 |
|---|---|---|
| 양방향 | 읽고 쓴다 | 읽기, 쓰기, 그 둘 다로 열 수 있다 |
| 아웃바운드 | 쓰기만 한다 | 읽기 전용으로 연다 |
| 인바운드 | 읽기만 한다 | 쓰기 전용으로 연다 |
모든 인스턴스가 사용 중이면 클라이언트의 CreateFile은 ERROR_PIPE_BUSY가 됩니다. WaitNamedPipe로 빈자리를 기다렸다가 CreateFile을 한 번 더 호출합니다. 서버가 아직 파이프를 만들지 않은 경우와는 다른 실패이므로, 기동 순서 경합과 함께 7.1에서 정리합니다.6
2.4 로컬 용도라도 원격 접속을 어떻게 다룰지 정한다
Named Pipe는 \\server\pipe\이름 형태로 SMB를 통한 원격 접속에도 쓸 수 있습니다. 다만 요즘 설계에서 이 경로를 적극적으로 쓸 이유는 거의 없고, 로컬 IPC에서는 쓰지 않는 경로를 열어 둔 채로 두지 않는 것이 중요합니다. 5.1의 PIPE_REJECT_REMOTE_CLIENTS로 명시적으로 거부합니다.17
3. 모드 선택: 「어디까지가 한 건인가」를 정한다
3.1 요청과 응답이면 메시지, 기존 경계가 있으면 바이트
전송 모드의 차이는 쓰기의 경계를 파이프가 보존하는지 여부입니다.5
| 모드 | 수신 측에 보이는 것 | 어울리는 사용법 |
|---|---|---|
바이트 모드 (PIPE_TYPE_BYTE) |
TCP와 같은, 끊김 없는 바이트 열 | 길이 접두사 같은 프레이밍을 가진 데이터나 스트림 전송 |
메시지 모드 (PIPE_TYPE_MESSAGE와 메시지 읽기) |
한 번의 쓰기를 한 메시지로 삼는 단위 | 한 건씩의 요청과 응답 |
flowchart TB
accTitle: 바이트 모드와 메시지 모드의 차이
accDescr: 바이트 모드에서는 세 번의 쓰기가 끊김 없는 바이트 열이 되어 수신 측이 직접 경계를 나눠야 하지만, 메시지 모드에서는 쓰기 한 번마다의 단위가 보존되어 수신 측에 그대로 도착한다
bw["바이트 모드: AAA, BB, CCCC를 쓰기"] --> br["수신은 AAABBCCCC 바이트 열"]
br --> bf["경계 (프레이밍)는 직접 설계"]
mw["메시지 모드: 같은 세 번의 쓰기"] --> mr["수신은 AAA, BB, CCCC 세 건"]
mr --> mf["쓰기의 단위가 보존된다"]
그림 2: 바이트 모드에서는 수신 측이 경계를 관리한다. 메시지 모드에서는 쓰기의 단위를 보존할 수 있지만, 한 번의 읽기로 전체를 받는다고 보장되지는 않는다.
아직 자체 경계를 갖고 있지 않고 「한 번의 쓰기 = 한 건의 요청」으로 다루고 싶다면 메시지 모드가 편리합니다. 이미 길이가 붙은 직렬화 형식 등을 쓰고 있다면 바이트 모드로 문제없습니다. .NET에서는 PipeTransmissionMode.Message가 메시지형 지정에 해당합니다.8
메시지형 양방향 파이프에는 요청 송신과 응답 수신을 한 번의 호출로 하는 TransactNamedPipe도 있습니다.9
3.2 메시지형과 읽기 모드는 별개의 설정
읽기 모드는 핸들마다 설정합니다.CreateNamedPipe에서 PIPE_READMODE_MESSAGE를 지정해도 그것은 서버 측 설정입니다. 클라이언트가 CreateFile로 연 핸들은 기본적으로 바이트 읽기가 됩니다.6
| 구현 | 서버 측 | 클라이언트 측 |
|---|---|---|
| Win32 | PIPE_TYPE_MESSAGE와 PIPE_READMODE_MESSAGE를 지정한다 |
접속 후 SetNamedPipeHandleState로 PIPE_READMODE_MESSAGE를 지정한다 |
| .NET | PipeTransmissionMode.Message를 지정한다 |
접속 후 NamedPipeClientStream.ReadMode를 Message로 설정한다 |
서버만 메시지형으로 해 두면 클라이언트도 같은 단위로 읽을 수 있다고 생각하지 않는 것이 요점입니다.
3.3 메시지 모드에서도 이어 읽기가 필요해진다
메시지의 경계가 지켜지는 것과 수신 버퍼에 전체가 담기는 것은 별개입니다.버퍼가 작으면 ReadFile은 ERROR_MORE_DATA를 반환하고 메시지는 분할됩니다. 받은 부분을 보관하고 나머지를 이어 읽어 연결하는 루프가 필요합니다.56
| 읽기 결과 | 처리 |
|---|---|
| 성공 | 메시지가 완성된 것으로 보고 처리한다 |
ERROR_MORE_DATA |
아직 남아 있으므로 이어 읽어 연결한다 |
| 그 밖의 오류 | 요청 처리를 계속하지 않고, 끊김 등 통신 실패로 다룬다 |
「작은 요청은 통과하는데 큰 요청만 깨진다」면 이 이어 읽기 처리를 확인합니다. 또한 무제한으로 연결해도 되는 것은 아닙니다. 메시지 하나의 최대 크기도 프로토콜로 정하고, 상한을 넘으면 끊는 방침으로 합니다. 거대한 메시지로 메모리를 낭비하는 것을 막기 위해서입니다.
4. 서버 구성: 접수와 클라이언트 처리를 나눈다
4.1 동기 스레드형과 overlapped형을 비교한다
서버의 기본 동작은 인스턴스 생성 → 접속 대기 → 읽고 쓰기 → 끊고 다음으로의 반복입니다. 여러 클라이언트를 동시에 받으려면 이 흐름을 여러 인스턴스로 진행합니다.
| 구성 | 구조 | 이점과 유의할 점 |
|---|---|---|
| 동기 스레드형 | 인스턴스마다 스레드를 하나씩 배정하고 동기 I/O로 처리한다 | 코드는 간단하다. 다만 클라이언트 수만큼 스레드를 소비하고, 정지할 때 블로킹 I/O에서 빠져나오게 하는 궁리가 필요하다 |
| overlapped(비동기)형 | FILE_FLAG_OVERLAPPED로 만들고 접속, 읽기, 쓰기를 비동기로 발행한다 |
적은 수의 스레드로 여러 인스턴스의 완료를 처리할 수 있다 |
Microsoft의 공식 샘플은 각 인스턴스의 이벤트를 배열로 만들어 WaitForMultipleObjects로 기다리며, 단일 스레드로 여러 접속을 처리하는 구성입니다. 클라이언트 수와 스레드 수를 떼어 놓을 수 있습니다. 규모가 커지면 IOCP나 스레드 풀 I/O로 잇는 선택지도 있습니다.10
비동기 I/O 자체의 구조는 동기 I/O와 비동기 I/O 글을 참조해 주십시오.
4.2 새로 만드는 .NET 구현이라면 async/await가 간단하다
.NET에서는 NamedPipeServerStream의 WaitForConnectionAsync / ReadAsync / WriteAsync와 async/await를 쓸 수 있습니다. 새로 만드는 구현이라면 특별한 이유가 없는 한 이 비동기형을 권합니다. 접수 루프는 접속을 받으면 개별 처리로 넘기고, 다음 인스턴스의 접수로 돌아갑니다.8
다음은 그 접수 루프의 뼈대입니다. 같은 사용자의 프로세스 사이에서 쓰는 예로 PipeOptions.Asynchronous와 PipeOptions.CurrentUserOnly를 지정했습니다. Windows에서 CurrentUserOnly는 같은 사용자이면서 같은 상승 수준으로 한정하는 지정입니다. 다른 계정의 서비스와 UI를 잇는 예가 아닙니다.그 경우에는 5.2의 ACL 설계가 필요합니다.11
// C#: 여러 클라이언트를 받는 서버의 뼈대
while (!token.IsCancellationRequested)
{
var server = new NamedPipeServerStream(
"MyCompany.MyApp.Control",
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
try
{
await server.WaitForConnectionAsync(token);
}
catch
{
await server.DisposeAsync(); // 접속 전에 빠져나갈 때는 직접 해제
throw;
}
_ = HandleClientAsync(server, token); // 접속 후의 소유권은 핸들러 쪽으로
}
접속 전에 예외가 발생한 경우에는 접수 측에서 해제하고, 접속 후에는 HandleClientAsync 쪽이 스트림의 소유권을 넘겨받습니다. 후자에는 읽고 쓰기뿐 아니라 종료 시의 해제도 포함합니다.
이 코드는 통신 프로토콜과 핸들러 본체를 생략한 뼈대입니다. 실제 운영에서는 떼어 낸 태스크의 예외와 종료를 관리하고, 3장의 이어 읽기와 크기 상한, 5, 6장의 보안, 7장의 끊김과 응답 확인을 조합합니다. CurrentUserOnly는 ACL을 직접 쓰지 않고 접속자를 한정하는 손쉬운 선택지이지만, 그것만으로 특권 서비스를 위한 설계가 완성되는 것은 아닙니다.
5. 보안: 서버 측 3가지와 클라이언트 측 1가지
Named Pipe의 보안상 이점은 올바르게 설정해야 비로소 얻어지는 것입니다. 특히 「관리자 권한 서비스 + 낮은 권한 UI 앱」이라는 브로커 구성에서는 파이프가 권한의 경계가 됩니다.
5.1 서버 측: 불필요한 원격 접속을 거부한다
로컬 IPC로 쓴다면 CreateNamedPipe에 PIPE_REJECT_REMOTE_CLIENTS를 지정합니다. 원격 클라이언트는 자동으로 거부되므로, 「같은 PC의 앱을 위해 만든 셈인데 네트워크에서도 열린다」는 경로를 닫을 수 있습니다.7
5.2 서버 측: ACL로 접속자와 권리를 좁힌다
SECURITY_ATTRIBUTES에 보안 설명자를 넘겨 접속해도 되는 사용자와 그룹을 명시합니다. 기본 ACL이 그 용도에 충분히 엄격하다고는 할 수 없습니다.2
여기서 놓치기 쉬운 것이 클라이언트에 주는 쓰기 권한입니다. ACL로 범용 쓰기(GENERIC_WRITE / FILE_GENERIC_WRITE)를 주면 FILE_CREATE_PIPE_INSTANCE에 해당하는 권리까지 포함됩니다. 그러면 허용된 클라이언트 자신이 같은 이름의 서버 인스턴스를 만들어 뒤따르는 접속을 가로챌 수 있게 됩니다.2
읽고 쓰기는 필요한 개별 권리로 주고, 클라이언트에는 인스턴스 생성 권한을 넘기지 않도록 합니다. 「누구에게 허용할 것인가」뿐 아니라 「무엇을 허용할 것인가」까지가 ACL 설계입니다.
5.3 서버 측: 첫 인스턴스를 선점당하지 않았는지 확인한다
악의를 가진 프로세스가 먼저 같은 이름의 파이프를 만들면 클라이언트는 그 가짜 서버에 접속해 버립니다. 서버는 첫 인스턴스를 만들 때만 FILE_FLAG_FIRST_PIPE_INSTANCE를 지정해 자신이 최초임을 보장합니다. 실패하면 선점을 의심하고 처리를 멈춥니다.5
이 플래그는 이름을 확보하는 첫 번째 인스턴스 전용입니다. 두 번째 이후의 인스턴스에도 붙이면 생성이 실패합니다.여러 클라이언트를 위한 접수 루프에서 모든 생성에 같은 지정을 반복하지 않도록 해 주십시오.5
다만 이것은 정규 서버가 기동할 때 이상을 감지하는 구조이지, 클라이언트의 접속 대상 인증은 아닙니다. 정규 서비스가 없다면 공격자가 먼저 같은 이름의 파이프를 만들어 기다릴 여지는 남습니다.
5.4 클라이언트 측: 서버에 권한을 어디까지 빌려줄지 정한다
가짜 서버에 대한 대비로, 클라이언트는 impersonation 수준을 필요 최소한으로 합니다. 신원 확인만 허용하는 설계라면 CreateFile에 SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION을 지정합니다. 서버는 계정을 확인할 수는 있어도, 그 권한을 빌려 실제 접근을 수행할 수는 없게 됩니다.3
| 서버에 시키고 싶은 것 | 클라이언트 측의 방침 |
|---|---|
| 접속자의 신원을 확인만 한다 | 식별 수준(SECURITY_IDENTIFICATION)으로 좁힌다 |
| 클라이언트 권한으로 파일을 여는 등 실제 접근을 수행한다 | impersonation 수준(SECURITY_IMPERSONATION) 허용이 필요하다. 다만 정규 서버로의 접속 확인과 한 세트로 한다 |
식별 수준인 채로는 클라이언트 권한으로 실제 접근을 하는 브로커의 처리를 할 수 없습니다. 반대로 접속 대상을 확인하지 않고 impersonation을 허용하면 가짜 서버에 권한을 빌려줄 위험이 있습니다.
서비스의 기동 보장이나 접속 후의 상호 인증으로 정규 접속 대상을 확인할 수 있는 경우에 한해 필요한 impersonation을 허용하는 것이 원칙입니다. 5.3의 선점 감지가 있으니 클라이언트 측 확인은 필요 없다고 생각하지 말아 주십시오.
6. impersonation 사용법: 실패한 요청은 실행하지 않는다
6.1 접속했을 뿐이 아니라 요청을 읽고 나서 impersonation한다
서버가 클라이언트의 신원을 확인하고 그 권한으로 처리하기 위한 API가 ImpersonateNamedPipeClient입니다. 대상은 그 파이프에서 마지막으로 읽은 메시지를 보낸 쪽의 보안 문맥입니다. 먼저 요청을 읽고, 그 뒤에 호출합니다.4
impersonation은 호출한 스레드에 적용됩니다. 그 상태로 파일을 열면 접근 가능 여부는 클라이언트의 권한을 기준으로 판정됩니다. 특권 서비스라 하더라도, 「부탁받은 조작을 부탁한 사람의 권한으로 실행한다」를 위한 구조입니다.3
6.2 읽기, 성공 확인, 원래대로 되돌리기를 한 세트로 한다
필요한 순서는 요청을 읽는다 → impersonation을 시도한다 → 성공을 확인한다 → 클라이언트 권한으로 조작한다 → 원래 문맥으로 되돌린다입니다.
sequenceDiagram
accTitle: impersonation을 사용한 요청 처리의 흐름
accDescr: 서버는 파이프에서 요청을 읽고 ImpersonateNamedPipeClient의 성공을 확인한 뒤 클라이언트의 권한으로 조작을 수행하며, RevertToSelf로 자신의 문맥으로 돌아온다. impersonation에 실패한 경우에는 요청을 실행하지 않고 거부한다
participant C as 클라이언트
participant S as 서버
C->>S: 요청을 송신
S->>S: 요청을 읽는다
S->>S: ImpersonateNamedPipeClient
Note over S: 실패하면 요청을 실행하지 않고 거부
S->>S: 클라이언트 권한으로 조작을 실행
S->>S: RevertToSelf로 되돌린다
S->>C: 결과를 응답
그림 3: impersonation에 실패하면 요청을 실행하지 않는다. 성공한 경우에도 작업 후 RevertToSelf로 되돌리는 데까지가 하나로 이어진 처리다.
ImpersonateNamedPipeClient의 반환값을 확인하지 않고 요청 처리로 나아가서는 안 됩니다.실패했을 때는 클라이언트의 권한으로 바뀌어 있지 않고, 서버 프로세스 자신의 권한이 쓰입니다. 서버가 특권 계정이라면 클라이언트에게 허용되지 않을 조작까지 통과시켜 버립니다. Microsoft의 문서도 실패한 경우에는 클라이언트의 요청을 실행하지 않도록 명기하고 있습니다.4
작업이 끝나면 RevertToSelf로 확실히 원래 문맥으로 되돌립니다.성공 확인과 뒷정리를 둘 다 갖춰 주십시오. 토큰, impersonation 수준, SeImpersonatePrivilege 같은 자세한 내용은 Windows의 impersonation 토큰 글에서 다룹니다.
7. 실무의 함정: 통신이 끊긴다는 전제로 설계한다
7.1 기동 대기와 만원 대기는 다른 실패로 다룬다
접속할 수 없을 때는 서버가 아직 파이프를 만들지 않았는지, 만들어진 인스턴스가 모두 사용 중인지를 나눕니다.69
| 상태 | 클라이언트의 처리 |
|---|---|
| 파이프가 존재하지 않는다 | 서버의 기동 대기를 감안해 조금 기다렸다가 재시도한다 |
ERROR_PIPE_BUSY |
WaitNamedPipe로 빈자리를 기다렸다가 CreateFile을 재시도한다 |
| 접속할 수 있었다 | 통신으로 나아간다. 필요하면 3.2의 읽기 모드도 설정한다 |
flowchart TB
accTitle: 클라이언트 접속의 재시도 흐름
accDescr: CreateFile로 파이프를 열고, 파이프가 존재하지 않으면 조금 기다렸다가 재시도하며, 모든 인스턴스가 사용 중인 ERROR_PIPE_BUSY라면 WaitNamedPipe로 빈자리를 기다린 뒤 재시도하고, 성공하면 통신에 들어간다
cf["CreateFile로 열기"] --> ok{"결과는?"}
ok -->|"성공"| go["통신 개시"]
ok -->|"파이프가 존재하지 않음"| wait1["조금 기다린다 (서버 미기동)"]
ok -->|"ERROR_PIPE_BUSY"| wnp["WaitNamedPipe로 빈자리를 기다린다"]
wait1 --> cf
wnp --> cf
그림 4: 「존재하지 않음」과 「만원」은 다른 상태다. 둘 다 상태에 맞는 대기 뒤에 접속을 재시도한다.
서버 측은 클라이언트가 기동하기 전에 ConnectNamedPipe로 대기를 시작해 두는 것이 원칙입니다. 그래도 기동 순서 경합은 일어날 수 있으므로 클라이언트 측에도 재시도를 넣습니다.9
7.2 끊김은 비정상 사태가 아니라 통상의 처리 경로로 만든다
상대 프로세스가 종료되면 읽고 쓰기는 ERROR_BROKEN_PIPE 등으로 실패합니다. 이것은 통신의 일상으로 다룹니다.
서버는 끊김을 감지하면 DisconnectNamedPipe로 인스턴스를 떼어 내고 다음 접속에 대비합니다. 클라이언트는 다시 접속합니다. 절전 복귀 글에서 설명한, 반복해도 상태를 망가뜨리지 않는 「멱등한 재접속」의 사고방식이 여기서도 유효합니다.
7.3 큰 메시지에는 「이어 읽기」와 「상한」이 둘 다 필요하다
3.3의 ERROR_MORE_DATA는 메시지를 나눠 읽기 위한 처리입니다. 한편 최대 메시지 크기는 받아들일 데이터 양을 제한하기 위한 약속입니다. 이 둘을 혼동하지 말아 주십시오.
악의를 가진 상대나 버그가 있는 상대가 거대한 메시지를 보내면, 끝없이 이어 읽는 구현은 메모리를 낭비합니다. 상한을 프로토콜에 정하고 넘으면 끊는 방침으로 합니다.
7.4 쓰기 성공과 상대의 처리 완료를 구별한다
WriteFile의 성공은 상대 앱이 데이터를 처리했다는 뜻이 아닙니다.확실히 실행되었는지 알아야 하는 조작은 응답 메시지로 확인합니다.
또한 어느 요청에 대한 응답인지 짝지을 수 있도록, 요청과 응답의 관계를 프로토콜에 포함합니다. 「보냈다」와 「처리가 끝났다」를 나누는 것이 업무상의 완료 확인으로 이어집니다.
8. 정리: 통신 경로, 데이터, 권한을 따로따로 정한다
Named Pipe는 파일 I/O와 같은 다루기 쉬움과, ACL과 impersonation이라는 Windows의 보안 모델을 조합할 수 있는 같은 머신 IPC의 첫 번째 후보입니다. 다만 통신 경로를 만드는 것과, 안전하고 잘 망가지지 않는 프로토콜을 만드는 것은 별개입니다.
접속과 데이터의 설계에서는 인스턴스 하나가 클라이언트 하나를 맡는다는 점을 출발점으로 삼습니다. 요청과 응답이라면 메시지 모드, 기존 프레이밍이 있다면 바이트 모드를 고르고, 메시지 읽기는 클라이언트 측에도 설정합니다. 분할 읽기에 대한 대응과 최대 크기도 필요합니다. 다중 접속을 받는 새 .NET 구현이라면 비동기 I/O와 async/await가 간단합니다.
권한의 설계에서는 원격 거부, ACL의 명시, 첫 인스턴스의 보장, 클라이언트 측 impersonation 수준의 최소화를 한 세트로 합니다. impersonation을 쓴다면 요청을 읽고 나서 호출하고, 실패하면 실행하지 않으며, 작업 후에는 RevertToSelf로 되돌립니다.
마지막으로 기동 순서, 끊김, 메시지 상한, 응답 확인을 자신의 프로토콜로 적어 내 주십시오. 「같은 PC 안이니까 실패하지 않는다」가 아니라, 통신이 끊겨도 상대의 권한을 넘지 않고 복구할 수 있는 형태를 정하고 나서 구현에 들어가는 것이 실무의 요점입니다.
관련 글
- Windows의 프로세스 간 통신을 어떻게 고를 것인가 ── Named Pipe / TCP / gRPC / 공유 메모리 / COM 판단표
- Windows 앱에서 「관리자 권한이 필요한 처리만」 분리하는 구체적인 작성법
- Windows의 impersonation 토큰을 올바르게 다루기 ── 스레드 단위의 권한 차용과 안전한 복귀 방법
- Windows I/O의 심층(제2회) ── 동기 I/O와 비동기 I/O: OVERLAPPED의 진짜 의미
- 공유 메모리의 함정과 실무 베스트 프랙티스
관련 상담 영역
합동회사 코무라소프트에서는 서비스와 UI 앱의 분리, 관리자 권한의 분리 같은 프로세스 간 통신을 동반하는 설계와 구현, 기존 IPC(공유 메모리, 자체 소켓, COM 등)의 Named Pipe로의 교체, 특권 서비스의 파이프 통신 보안 리뷰를 다룹니다. 프로토콜 설계를 함께 검토하는 단계부터 상담해 주십시오.
참고 링크
-
Microsoft Learn, Named Pipes. Named Pipe가 파이프 서버와 하나 이상의 파이프 클라이언트 사이의 단방향 또는 양방향 통신 경로라는 점, 모든 인스턴스가 같은 이름을 공유하면서 독립된 버퍼와 핸들을 가진다는 점, 로컬과 원격 프로세스에서 이용할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Named Pipe Security and Access Rights. Named Pipe 접근 권한의 구성, GENERIC_WRITE에 FILE_CREATE_PIPE_INSTANCE가 포함되어 클라이언트에 범용 쓰기를 주면 서버 인스턴스 생성까지 허용하게 된다는 점, 데이터 읽기와 쓰기는 개별 접근 권한으로 부여해야 한다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Impersonating a Named Pipe Client. impersonation에 의해 서버 스레드가 클라이언트 권한의 범위에서 동작할 수 있다는 점, 기본 impersonation 수준이 SecurityImpersonation이라는 점, 클라이언트가 CreateFile 때 SECURITY_SQOS_PRESENT 플래그로 impersonation 수준을 제어할 수 있다(SECURITY_IDENTIFICATION이면 신원 확인만 허용한다)는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). 서버 측 스레드가 파이프에서 마지막으로 읽은 메시지의 클라이언트 security context에서 impersonation을 시작한다는 점, 완료 후 RevertToSelf로 되돌린다는 점, impersonation에 실패한 채로 처리를 이어가면 서버 프로세스 자신의(특권적인) 문맥에서 실행되므로 반환값을 반드시 확인하고 실패 시 클라이언트의 요청을 실행해서는 안 된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). 파이프 방향(인바운드, 아웃바운드, 양방향), 바이트형과 메시지형(PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) 및 읽기 모드(PIPE_READMODE_MESSAGE), 최대 인스턴스 수(PIPE_UNLIMITED_INSTANCES), FILE_FLAG_OVERLAPPED에 의한 비동기 모드, FILE_FLAG_FIRST_PIPE_INSTANCE에 의한 첫 인스턴스 보장, WaitNamedPipe용 기본 타임아웃에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Named Pipe Client. 클라이언트가 CreateFile로 파이프를 연다는 점, 모든 인스턴스가 사용 중이면 ERROR_PIPE_BUSY가 되어 WaitNamedPipe로 빈자리를 기다린다는 점, 연 핸들의 기본이 바이트 읽기와 블로킹과 non-overlapped이며 SetNamedPipeHandleState로 메시지 읽기 모드로 바꿀 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). PIPE_ACCEPT_REMOTE_CLIENTS(원격 연결을 받아 security descriptor로 검사한다)와 PIPE_REJECT_REMOTE_CLIENTS(원격 클라이언트 연결을 자동으로 거부한다)의 두 원격 클라이언트 모드에 대해. ↩ ↩2
-
Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). NamedPipeServerStream / NamedPipeClientStream에 의한 연결과 읽기와 쓰기, PipeTransmissionMode.Message에 의한 메시지 단위 전송, 비동기 메서드에 의한 여러 클라이언트 처리에 대해. ↩ ↩2
-
Microsoft Learn, Named Pipe Operations. ReadFileEx / WriteFileEx에 의한 overlapped 작업, PeekNamedPipe에 의한 꺼내지 않는 읽기, 메시지형 양방향 파이프에서 요청 송신과 응답 수신을 한 번에 하는 TransactNamedPipe, 클라이언트 기동 전의 블로킹 읽기가 경합을 일으킬 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Named Pipe Server Using Overlapped I/O. 단일 스레드 서버가 overlapped 작업으로 여러 클라이언트와의 동시 연결을 처리하는 공식 샘플에 대해. 각 인스턴스의 OVERLAPPED 구조체와 이벤트를 WaitForMultipleObjects로 기다리고 완료된 인스턴스의 상태 기계를 진행하는 구성, GetOverlappedResult로 보류 I/O의 완료를 확인하는 점에 대해. ↩
-
Microsoft Learn, PipeOptions Enum (System.IO.Pipes). Asynchronous로 비동기 I/O를 켜는 점과, CurrentUserOnly로 같은 사용자(그리고 같은 상승 수준)의 프로세스와의 연결만 허용할 수 있다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
인수는 왜 깨지는가 ── Windows 명령줄 인수의 규칙
Windows에는 인수의 배열이 존재하지 않으며, CreateProcess에 전달되는 것은 한 줄의 문자열이고 분할은 받는 쪽이 합니다. CommandLineToArgvW·CRT·.NET의 분할 규칙과 .NET의 ArgumentList, C++에...
부모가 죽은 뒤에 무엇이 남는가 — Job Object로 자식 프로세스를 기르기
UI를 강제로 종료해도 SDK의 헬퍼가 남아 카메라나 COM 포트를 붙잡고 놓지 않는 이유는 무엇인가. Job Object로 프로세스 트리를 하나의 단위로 만들고, KillOnJobClose와 완료 포트로 자식 프로세스의 수명을 설계하는 방법을 ...
Win32 스레드 풀 API ── CreateThreadpoolWork로 「스레드를 만들지 않는」 병렬 처리
네이티브 코드에서 CreateThread를 마구 늘리고 있지는 않습니까. Vista에서 새로 설계된 Win32 스레드 풀 API의 work, timer, wait, io 네 가지 객체와 클린업 그룹, 콜백에서 해서는 안 되는 일까지 1차 자료를 ...
DllMain과 로더 락 ── 「DLL 초기화에서는 아무것도 하지 말라」는 말의 진짜 이유
DllMain에서 LoadLibrary나 스레드 동기화를 해서는 안 되는 이유는 무엇인가. 모든 DLL 알림을 직렬화하는 로더 락의 구조부터, 데드락이 성립하는 전형적인 시나리오, 지연 초기화 같은 올바른 설계, hang 조사 절차까지를 1차 정...
spurious wakeup ── 조건 변수가 「알림 없이 깨어나는」 이유와 Windows에서 올바르게 기다리는 방법
조건 변수의 wait는 알림이 오지 않아도 깨어날 수 있습니다(spurious wakeup). 사양이 이를 허용하는 이유를 Windows 구현에서 밝히고, while과 predicate로 쓰는 올바른 대기 방법을 Win32·C++·C# 코드로 보...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Named Pipe와 TCP(localhost 소켓)는 어떻게 나눠 써야 합니까?
- 같은 머신 안의 프로세스 간 통신이라면 Named Pipe가 첫 번째 후보입니다. 이유는 보안 모델에 있습니다. 파이프는 Windows의 보안 설명자(ACL)로 「누가 접속할 수 있는가」를 OS 수준에서 제어할 수 있고, 서버 측은 ImpersonateNamedPipeClient로 접속 상대의 Windows 계정을 확인하고 빌려 쓸 수 있습니다. localhost의 TCP 포트에는 누구나 접속할 수 있어 상대가 누구인지 직접 만든 인증으로 확인해야 하는 것과 대조적입니다. 한편 앞으로 원격 통신으로 발전할 가능성이 크다, 다른 OS의 프로세스와도 이야기한다, gRPC 같은 기존 프로토콜 자산을 쓰고 싶다면 TCP 기반이 유리합니다. 이 판단은 프로세스 간 통신 판단표 글에서도 정리하고 있습니다.
- 바이트 모드와 메시지 모드 중 어느 쪽을 써야 합니까?
- 「한 번의 쓰기 = 한 건의 의미 단위」로 다루고 싶다면 메시지 모드(PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE)가 편리합니다. 수신 측은 송신 측이 쓴 단위로 읽을 수 있어 직접 경계를 관리할 필요가 없습니다. 바이트 모드는 TCP와 같은 「끊김 없는 바이트 열」이라, 길이 접두사 같은 경계(프레이밍)를 스스로 설계해야 합니다. 이미 프레이밍을 가진 프로토콜(예를 들어 길이가 붙은 직렬화 형식)을 흘려보낸다면 바이트 모드로 문제없습니다. 유의할 점으로, 메시지 모드에서도 수신 버퍼가 메시지보다 작으면 분할 읽기(ERROR_MORE_DATA)가 발생하므로 그 처리는 필요합니다. 또 읽기 모드는 핸들마다의 설정이며 CreateNamedPipe로 설정되는 것은 서버 측뿐입니다. 클라이언트 측은 CreateFile 뒤에 SetNamedPipeHandleState로 PIPE_READMODE_MESSAGE를 지정해야 합니다. .NET에서는 서버가 PipeTransmissionMode.Message를 지정하고, 클라이언트는 접속 후 NamedPipeClientStream.ReadMode를 Message로 설정합니다.
- 여러 클라이언트를 동시에 상대하는 서버는 어떻게 만듭니까?
- Named Pipe는 같은 이름으로 여러 인스턴스를 만들 수 있고, 인스턴스 하나가 클라이언트 하나를 맡습니다. 구성은 두 가지입니다. 하나는 클라이언트마다 스레드를 배정하는 동기형으로, 구현은 간단하지만 클라이언트 수만큼 스레드를 소비합니다. 다른 하나는 FILE_FLAG_OVERLAPPED로 비동기 I/O를 만들어 적은 수의 스레드로 모든 인스턴스의 ConnectNamedPipe, ReadFile, WriteFile을 처리하는 형태로, Microsoft의 공식 샘플에도 단일 스레드로 여러 인스턴스를 처리하는 구현이 제시되어 있습니다. .NET이라면 NamedPipeServerStream의 WaitForConnectionAsync와 async/await로 비동기형을 동기형만큼 간단하게 쓸 수 있습니다. 새로 만드는 구현이라면 특별한 이유가 없는 한 .NET의 비동기형을 권합니다.
- Named Pipe 보안에서 최소한 해야 할 일은 무엇입니까?
- 네 가지입니다. 첫째, 원격 접속이 필요 없다면 PIPE_REJECT_REMOTE_CLIENTS를 지정해 네트워크를 통한 접속을 명시적으로 거부합니다. 둘째, SECURITY_ATTRIBUTES로 적절한 ACL을 설정해 접속해도 되는 사용자와 그룹을 좁힙니다(기본 ACL은 용도에 따라 너무 느슨합니다). 셋째, 첫 인스턴스를 만들 때 FILE_FLAG_FIRST_PIPE_INSTANCE를 지정해 같은 이름의 파이프가 먼저 만들어지는 「이름 가로채기」를 감지합니다(두 번째 이후의 인스턴스에는 붙이지 않습니다). 넷째, 서버에 신원 확인만 시키고 싶은 클라이언트는 CreateFile에 SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION을 지정해 가짜 서버가 자신의 권한을 빌려 쓰지 못하도록 제한합니다. 서버가 클라이언트 권한으로 실제 접근을 하는 브로커 구성에서는 impersonation 허용이 필요하므로, 이 제한은 「서버에 권한 차용을 시키지 않는 설계인지」로 나눠 씁니다.
- ImpersonateNamedPipeClient를 쓸 때 유의할 점이 있습니까?
- 가장 중요한 것은 반환값 확인입니다. impersonation에 실패했는데 처리를 이어가면 이후의 조작은 서버 프로세스 자신의(대개 높은) 권한으로 실행되어, 클라이언트에게 허용되지 않아야 할 조작이 통과해 버립니다. 공식 문서도 실패했을 때는 클라이언트의 요청을 실행해서는 안 된다고 명기하고 있습니다. 또 impersonation은 「마지막으로 파이프에서 읽은 메시지」의 문맥에서 이루어지므로 반드시 무언가를 읽은 뒤에 호출할 것, 작업이 끝나면 RevertToSelf로 확실히 원래 문맥으로 되돌릴 것도 필요합니다. impersonation 주변의 구조(토큰, impersonation 수준, SeImpersonatePrivilege)는 impersonation 토큰 글에서 자세히 설명하고 있습니다.