네트워크 환경에서 명명된 파이프를 이용한 멀티스레드 프로세스 간 통신

파이프의 기본적인 사용법에 대해서는 이전에 다룬 명명된 파이프를 이용한 프로세스 간 통신 (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 예외가 발생할 수 있습니다.

주의: 본 예제 코드는 교육 및 이해를 돕기 위한 것이며, 실제 운영 환경에 직접 적용하기 전에 충분한 테스트와 안정성 검증이 필요합니다.

관련 자료:

태그: 명명된 파이프 프로세스 간 통신 멀티스레딩 네트워크 프로그래밍 System.IO.Pipes

8월 20일 04:05에 게시됨