파이프의 기본적인 사용법에 대해서는 이전에 다룬 명명된 파이프를 이용한 프로세스 간 통신 (System.IO.Pipes 사용) 글을 참조하시기 바랍니다.
본 글에서는 명명된 파이프를 이용한 실제 통신 사례를 소개합니다. 예제 시나리오는 여러 클라이언트가 서버로부터 순차적으로 증가하는 고유 ID를 요청하는 상황입니다.
서버 측 기능: 클라이언트가 새로운 ID를 요청하면, 서버는 현재 ID 값을 1 증가시킨 후 해당 ID를 클라이언트에게 응답합니다. 이 과정은 여러 스레드를 통해 동시에 처리됩니다.
서버 측 구현: 프로그램 시작 시, 지정된 개수(N개)의 스레드를 생성합니다. 각 스레드는 NamedPipeServerStream 인스턴스를 생성하고, WaitForConnection() 메서드를 통해 클라이언트의 연결을 기다립니다. 연결이 수립되면 새로운 ID를 생성하여 파이프를 통해 전송하고 연결을 종료합니다. 프로그램 종료 시에는 모든 NamedPipeServerStream 인스턴스를 정리합니다.
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace IDServer
{
class Program
{
// 명명된 파이프 이름
private const string PIPE_NAME = "uniqueIdGeneratorPipe";
// 동시에 실행될 서버 스레드 수 (NamedPipeServerStream 최대 인스턴스 수)
private const int MAX_SERVER_INSTANCES = 4;
private static int activeThreadCount = 0;
// 생성된 ID 카운터 (volatile 키워드로 스레드 간 일관성 보장)
private static volatile int currentIdCounter = 0;
// 서버 스트림 인스턴스 배열
private static NamedPipeServerStream[] serverStreams;
static void Main(string[] args)
{
serverStreams = new NamedPipeServerStream[MAX_SERVER_INSTANCES];
// 프로그램 종료 시 리소스 해제를 위한 이벤트 핸들러 등록
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
// 서버 스레드 시작
StartServerThreads();
Console.WriteLine("ID 서버가 시작되었습니다. 아무 키나 눌러 종료하세요.");
Console.ReadKey();
}
/// <summary>
/// 프로그램 종료 시 명명된 파이프 리소스 해제
/// </summary>
static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
Console.WriteLine("ID 서버 종료 중...");
if (serverStreams != null)
{
foreach (var stream in serverStreams)
{
stream?.Dispose();
}
}
Console.WriteLine("ID 서버가 종료되었습니다.");
}
/// <summary>
/// 서버 스레드들을 시작합니다.
/// </summary>
private static void StartServerThreads()
{
for (int i = 0; i < MAX_SERVER_INSTANCES; i++)
{
Thread serverThread = new Thread(StartIdServerInstance)
{
IsBackground = true // 백그라운드 스레드로 설정하여 메인 스레드 종료 시 함께 종료되도록 함
};
serverThread.Start();
}
}
/// <summary>
/// 단일 명명된 파이프 서버 인스턴스를 생성하고 클라이언트 연결을 처리합니다.
/// </summary>
private static void StartIdServerInstance()
{
int instanceIndex = Interlocked.Increment(ref activeThreadCount) - 1; // 스레드별 고유 인덱스 할당
Console.WriteLine($"스레드 ID {Thread.CurrentThread.ManagedThreadId} 에서 서버 인스턴스 {instanceIndex} 시작.");
// 각 스레드마다 고유한 서버 스트림 인스턴스 생성
serverStreams[instanceIndex] = new NamedPipeServerStream(
PIPE_NAME,
PipeDirection.Out, // 클라이언트는 ID를 받기만 하므로 Out 방향으로 설정
MAX_SERVER_INSTANCES,
PipeTransmissionMode.Byte, // 바이트 스트림 모드 사용
PipeOptions.Asynchronous // 비동기 I/O 옵션
);
NamedPipeServerStream currentStream = serverStreams[instanceIndex];
try
{
while (true)
{
// 클라이언트 연결 대기
currentStream.WaitForConnection();
Console.WriteLine($"클라이언트 연결 감지 (스레드: {Thread.CurrentThread.ManagedThreadId})");
// 새 ID 생성 (스레드 안전성을 위해 Interlocked 사용)
int newId = Interlocked.Increment(ref currentIdCounter);
byte[] idBytes = BitConverter.GetBytes(newId);
// ID를 바이트 배열로 변환하여 파이프로 전송
currentStream.Write(idBytes, 0, idBytes.Length);
currentStream.Flush(); // 버퍼 비우기
Console.WriteLine($"ID {newId} 전송 완료 (스레드: {Thread.CurrentThread.ManagedThreadId})");
// 연결 종료 (각 요청마다 연결을 닫음)
if (currentStream.IsConnected)
{
currentStream.Disconnect();
}
}
}
catch (IOException ex)
{
Console.WriteLine($"I/O 오류 발생 (스레드: {Thread.CurrentThread.ManagedThreadId}): {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"예외 발생 (스레드: {Thread.CurrentThread.ManagedThreadId}): {ex.Message}");
}
finally
{
// 스트림이 null이 아니고 아직 Dispose되지 않았다면 해제
if (currentStream != null && !currentStream.IsAsync) // IsAsync는 Dispose 후 true가 됨
{
currentStream.Dispose();
}
}
}
}
}
클라이언트 측 기능: 클라이언트는 지속적으로 새로운 ID를 요청하고 받은 ID를 화면에 출력합니다. 클라이언트 코드에서는 서버의 IP 주소를 설정할 수 있습니다.
클라이언트 측 구현:
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Text;
namespace IDClient
{
class Program
{
private const string PIPE_NAME = "uniqueIdGeneratorPipe";
private const int CONNECTION_TIMEOUT_MS = 2000; // 연결 시도 타임아웃 (밀리초)
static void Main(string[] args)
{
Console.WriteLine("ID 클라이언트가 시작되었습니다. 연결을 시작하려면 아무 키나 누르세요.");
Console.ReadKey();
// 서버 IP 주소를 설정합니다. (내부 네트워크 IP 또는 로컬호스트)
string serverAddress = "127.0.0.1"; // 예: "192.168.1.100"
try
{
// 명명된 파이프 클라이언트 스트림 인스턴스 생성
using (var clientPipe = new NamedPipeClientStream(serverAddress, PIPE_NAME, PipeDirection.In, PipeOptions.Asynchronous))
{
Console.WriteLine($"서버 '{serverAddress}'에 연결 시도 중...");
// 서버 연결 시도 (타임아웃 설정)
clientPipe.Connect(CONNECTION_TIMEOUT_MS);
Console.WriteLine("서버에 성공적으로 연결되었습니다.");
// 메시지 단위로 데이터를 읽도록 설정 (서버 구현과 일치)
clientPipe.ReadMode = PipeTransmissionMode.Byte;
// ID 요청 및 수신 루프
while (true)
{
byte[] buffer = new byte[sizeof(int)]; // int 크기만큼의 버퍼 준비
// 서버로부터 ID 데이터 읽기
int bytesRead = clientPipe.Read(buffer, 0, buffer.Length);
if (bytesRead == buffer.Length)
{
int receivedId = BitConverter.ToInt32(buffer, 0);
Console.WriteLine($"수신된 새 ID: {receivedId}");
}
else if (bytesRead > 0)
{
Console.WriteLine($"부분적으로만 데이터 수신됨 (읽은 바이트: {bytesRead}).");
// 추가 데이터 처리가 필요한 경우 여기에 로직 추가
}
else // bytesRead == 0 인 경우, 서버 측에서 연결이 닫혔을 수 있음
{
Console.WriteLine("서버로부터 더 이상 데이터를 수신할 수 없습니다. 연결이 종료되었을 수 있습니다.");
break; // 루프 종료
}
// 서버의 응답을 기다리는 동안 잠시 대기 (CPU 사용량 줄이기)
Thread.Sleep(50); // 50ms 대기
}
}
}
catch (TimeoutException)
{
Console.WriteLine($"오류: 서버 '{serverAddress}'에 연결하는 데 시간이 초과되었습니다 ({CONNECTION_TIMEOUT_MS}ms).");
}
catch (IOException ex)
{
Console.WriteLine($"I/O 오류 발생: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"알 수 없는 오류 발생: {ex.Message}");
}
finally
{
Console.WriteLine("ID 클라이언트가 종료됩니다.");
}
}
}
}
SQL Server와 같은 시스템에서도 명명된 파이프를 사용하여 네트워크 상의 프로세스 간 통신을 구현합니다.
NamedPipeServerStream 인스턴스를 생성할 때, 동시에 사용할 수 있는 최대 인스턴스 수를 지정할 수 있습니다. 이 제한을 초과하여 연결을 시도하면 "모든 파이프 인스턴스가 사용 중입니다"와 같은 IO 예외가 발생할 수 있습니다.
주의: 본 예제 코드는 교육 및 이해를 돕기 위한 것이며, 실제 운영 환경에 직접 적용하기 전에 충분한 테스트와 안정성 검증이 필요합니다.
관련 자료: