Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN 홈 > MSDN 칼럼 > Working with C# > 경량 메시지 전달 시스템 구축

경량 메시지 전달 시스템 구축

Eric Gunnerson
Microsoft Corporation

2003년 9월 8일

요약: Eric Gunnerson은 소켓 기반 아키텍처에 대해 설명하고 PC 및 Pocket PC에서 모두 실행할 수 있는 효율적이고 조작이 쉬운 메시지 전달 시스템을 만드는 방법에 대해 소개합니다.

 csharp09182003_sample.msi  파일을 다운로드하십시오.

지난 달 저는 원격 기능에 대해 연구해 보았지만 결국 Pocket PC에서는 사용할 수 없다는 결론에 도달했습니다. 이번 달에는 제가 만든 소켓 기반 시스템을 소개하겠습니다. 그러나 우선 몇 가지 다른 주제에 대해 얘기하도록 하겠습니다.

블로그(Blog)와 인간

이전에 웹로그(weblog 또는 blog라고도 함)를 가지고 있다고 언급한 적이 있었는데 웹로그를 읽는 방식에 대해서는 설명하지 않았습니다. 다음 웹 사이트 http://blogs.gotdotnet.com/ericgu/ 로 이동해 읽을 수 있지만 모든 블로그마다 해당 사이트로 이동해야 했습니다. 이는 꽤 번거로운 작업입니다. 사실 사이트 몇 개 정도는 이동할 수 있지만 5개가 넘으면 어려워집니다.

실제로 필요한 것은 웹 사이트를 방문하지 않고도 새 콘텐트가 있는지 알 수 있는 방법입니다. 이는 RSS 피드라 알려진 XML 피드를 노출하는 블로그 소프트웨어를 사용하면 가능합니다. 그런 후 RSS 판독기 또는 RSS 애그리게이터(aggregator)로 알려진 소프트웨어 프로그램을 사용하여 갱신된 내용을 모니터링하고 새로운 내용을 알려주도록 합니다. 애그리게이터 목록은 다음 사이트 http://backend.userland.com/directory/167/aggregators 에서 확인해 볼 수 있습니다.

애그리게이터에는 두 종류가 있습니다. 하나는 독립 실행형 프로그램으로서 원하는 블로그에 대한 RSS 피드를 정기적으로 가져오고 새로운 내용이 있을 경우 알려줍니다. 저는 SharpReader 라고 불리는 C#으로 작성된 프리웨어 애그리게이터를 사용하고 있습니다. 또 다른 유형의 애그리게이터는 Microsoft Outlook®과 같은 전자 메일 프로그램에 대한 추가 기능으로 실행되며 Outlook 폴더에서 새 블로그 항목을 메시지로 변환합니다. NewsGator 가 바로 그러한 애그리게이터입니다.

이제 이전에 계획한 프로그램으로 돌아갑니다.

이전 부서로 복귀

Microsoft Visual Studio® 2002 개발 당시 저는 언어 디자이너이자 C# 컴파일러 테스트 리드였습니다. 언어 디자이너의 일을 매우 좋아했지만 사실 아주 전문적인 수준이었다고는 할 수 없었습니다.

저는 Visual Studio 2002를 배포한 후 테스트 부서에서 프로그램 관리(program management) 부서로 이동하기로 결심했습니다. 즉 프로젝트 시스템과 커뮤니티 개발 노력에 주력하기 위해서 언어 쪽을 떠난 것입니다.

최근 프로그램 관리팀 내에서 부서 이동이 있었는데 마침 새 컴파일러 PM 자리가 비게 되어 제가 그 일을 떠맡게 되었습니다. 다시 언어 관련 일을 하게 되어 얼마나 기쁜지 모릅니다.

소켓 및 메시지

오늘날 대부분의 웹 서비스와 원격 응용 프로그램은 원격 프로시저 호출(RPC) 접근법을 사용합니다. 함수 호출과 유사한 호출을 하면 내부에서 마술 같은 여러 가지 일이 일어나 서버상에서 작업이 이루어지게 됩니다. 하위 수준에서 시스템은 두 컴퓨터 간에 메시지를 전달하지만 이 작업은 외부에서 관찰할 수 없습니다.

그러나 소켓으로 전환하면 단순히 메시지만을 기반으로 한 시스템에서 프로그래밍 작업을 하게 됩니다. 이는 작성하고 있는 코드의 종류를 변경시키며 메시지를 통해서만 데이터를 다시 되돌려 받을 수 있습니다. 이는 반환된 값이나 결과 매개 변수가 없으며 모든 반환된 정보가 이벤트를 통해 들어오는 .NET 클래스를 사용하는 것과 어느 정도 유사합니다.

클라이언트에서 명시적으로 요청하지 않아도 정보가 서버에서 클라이언트로 전달되면, 서버가 클라이언트에게 노래가 바뀌는 시기를 알려 주기를 원하는 저에게 메시지 사용은 매우 큰 도움이 됩니다. 그러나 그렇게 하려면 다른 방식으로 접근해야 합니다.

그 방식을 언급하기 전에 보안에 대해 설명하고자 합니다. 컴퓨터에서 포트를 열 때 다른 사람이 불순한 목적으로 포트를 사용하려고 할 수 있습니다. 해당 컴퓨터를 마음대로 제어하거나 작동을 중지시킬 수 있는지 알아 보려고 포트에 정크를 작성할 수도 있습니다.

이러한 종류의 프로그램을 작성할 때 위와 같은 가능성에 대비하는 것이 좋습니다. 제 경우에는 방화벽이 있는 홈 네트워크에서 프로그램을 실행시키므로 어느 정도 안전하다고 할 수 있습니다.

단순 소켓

우선 정수에 1을 더할 수 있는 서버에서부터 시작하겠습니다. 다음은 서버 쪽 코드입니다.

public static void Main()
{
    IPAddress localAddr = IPAddress.Parse("127.0.0.1");
   
    TcpListener listener = new TcpListener(localAddr, 9999);

    Console.WriteLine("Waiting for initial connection");
    listener.Start();
    Socket socket = listener.AcceptSocket();
    Console.WriteLine("Connected");
    NetworkStream stream = new NetworkStream(socket);
    BinaryReader reader = new BinaryReader(stream);
    BinaryWriter writer = new BinaryWriter(stream);

    int i = reader.ReadInt32();
    i++;
    writer.Write(i);
}

로컬 호스트의 포트 9999에 TCP 수신기를 만듭니다. 수신기를 시작한 후 연결되기를 기다립니다. 연결이 되면 정수를 받아 증가시킨 후 다시 보냅니다.

여기서 로컬 호스트 주소로 127.0.0.1을 사용하고 있습니다. 이 작업은 클라이언트와 서버가 같은 컴퓨터에 있는 경우 테스트하기 좋지만, 클라이언트와 서버가 다른 컴퓨터에서 실행되는 경우에는 동작하지 않습니다. 더 복잡한 코드 예제는 나중에 보여 드리겠습니다. 예제 코드는 SimpleSockets 하위 디렉터리에 있습니다.

메시지 전달

소켓에서 원시 데이터를 전달하는 것은 쉬운 작업이 아닙니다. 차라리 소켓에서 개체를 전달하는 것이 쉽습니다. 이 작업을 수행하려면 개체를 바이트의 스트림으로 변환해야 합니다. 확실한 솔루션은 런타임 시 제공되는 serialization 지원을 사용하는 것입니다. 그러나 불행하게도 이 접근 방식에는 몇 가지 문제가 있습니다.

첫 번째 문제는 serialization에는 상당한 비용이 든다는 것입니다. 즉 데이터를 전송하는 데 필요 이상으로 바이트를 사용하게 됩니다. 만약 SOAP 형식을 사용한다면 문제는 더욱 심각해집니다. 문제의 발생 여부는 응용 프로그램의 성능 요구 사항에 달려 있습니다. 두 번째 문제는 serialization을 Compact Fireworks에서 사용할 수 없다는 점입니다. 다른 방도가 없기 때문에 사용자가 직접 이 작업을 수행해야 합니다. 물론 해야 할 작업의 양은 serialization 작업보다는 훨씬 더 적습니다.

우선 전달해야 할 메시지를 정의하는 Enum을 선언합니다.

public enum MessageType
{
    RequestEmployee = 1,
    Employee,
}

각 메시지 형식마다 개체를 정의하는 개체 하나가 필요합니다.

public class RequestEmployee: ISocketObject
{
    int id;
    public RequestEmployee(int id)
    {
        this.id = id;
    }

    public RequestEmployee(BinaryReader reader)
    {
        id = reader.ReadInt32();
    }

    public int ID
    {
        get
        {
            return id;
        }
    }

    public void Send(BinaryWriter writer)
    {
        writer.Write((int) MessageType.RequestEmployee);
        writer.Write(id);
    }
}

여기서 취한 접근 방식은 ISerializable 인터페이스와 매우 유사합니다. ISocketObject 인터페이스는 데이터를 serialize하는 Send() 함수를 정의하며 데이터를 deserialize하는 생성자가 있습니다.

이 개체 중 하나가 자체적으로 serialize할 때마다 첫 번째로 보내는 것은 메시지 식별자입니다. 이는 어떠한 종류의 개체가 오고 있는지 수신기에서 알 수 있게 하고 그 개체를 만들도록 해 줍니다. 다음은 클라이언트 쪽의 코드입니다.

RequestEmployee requestEmployee = new RequestEmployee(15);
requestEmployee.Send(writer);

MessageType messageType = (MessageType) reader.ReadInt32();

switch (messageType)
{
    case MessageType.Employee:
        Employee employee = new Employee(reader);
        Console.WriteLine("{0} = {1}", employee.Name, employee.Address);
        break; 
}

이 코드는 RequestEmployee 개체를 만들어 서버로 보냅니다. 그리고 어떤 종류의 개체가 돌아 왔는지 확인한 후 deserialize합니다.

샘플 프로젝트는 client와 server라고 표시되어 있는데 실질적인 차이점은 연결 방식입니다. 연결이 이루어지면 이 둘은 각각 고유한 메시지 집합이 있음에도 불구하고 비슷한 코드를 사용하여 메시지를 전달하고 받습니다. 예제 코드는 SocketObjects 하위 디렉터리에 있습니다.

개체 지향적(OO) 디자인과 실질적인 디자인 비교

이 접근 방식의 한 가지 문제는 긴 switch 문으로 끝난다는 것입니다. 대부분의 사람들은 긴 switch 문을 좋지 않은 디자인으로 생각합니다. 일반적인 개체 지향적(OO) 접근 방식은 다형성을 사용하는 것입니다.

다형성을 사용하려면 추상 기본 클래스를 정의한 후 해당 클래스에서 모든 메시지 개체를 파생시켜야 합니다. 각 클래스에서는 메서드를 구현하여 serialization, deserialization를 수행하고 메시지를 처리합니다. 주요 코드는 아래와 같습니다.

  • 메시지 형식 읽기
  • 리플렉션을 사용하여 인스턴스 만들기
  • 가상 HandleMessage() 함수 호출

위 코드는 제대로 실행되지만 몇 가지 좋지 않은 영향을 줍니다. 첫째로 인스턴스를 만드는 코드가 쓰기에는 좀 복잡하고 리플렉션을 사용하기 때문에 속도가 더 느려지게 됩니다. 더 중요한 것은 메시지의 처리가 공유 라이브러리의 일부인 HandleMessage() 함수에 의해 이루어집니다. 이는 메시지 전달 방식과 거의 상관없이 메시지가 처리되기 때문에 좋은 방식이 아닙니다. 이러한 문제들로 인해 저는 덜 개체 지향적이지만 쓰기에는 더 쉬운 접근 방식을 고수하기로 했습니다.

실제 상황에 적용

앞의 예제에서는 하나의 메시지만을 처리하지만, 실제로는 여러 메시지를 끊임없이 처리해야 합니다.

서버 스레딩

저의 궁극적인 목표는 서버의 기능을 기존 응용 프로그램에 추가하는 것입니다. 기존 응용 프로그램의 코드를 수정하고 싶지 않기 때문에 서버를 스레드에서 실행할 것입니다. 또한 동시 다중 연결을 허용할 것입니다. 이 작업을 먼저 하겠습니다.

마지막 예제가 포트 9999에서 수신 대기했으나 한 개의 포트에서 단 하나의 클라이언트만이 통신할 수 있기 때문에 각 연결에 대해 다른 포트를 사용해야 합니다. SocketListener 클래스는 포트 9999에서 수신 대기하고 새 연결 요청이 들어올 때마다 사용하지 않은 포트를 찾아 클라이언트에게 보낼 것입니다. 다음은 이 클라이언트에 대한 개요입니다.

    public class SocketListener
    {
        int port;
        Thread thread;

        public SocketListener(int port)
        {
            this.port = port;
            ThreadStart ts = new ThreadStart(WaitForConnection);
            thread = new Thread(ts);
            thread.IsBackground = true;
            thread.Start();
        }

        public void WaitForConnection()
        {
            // 주 코드는 여기에 추가하십시오.
        }
    }

WaitForConnection() 메서드가 바로 모든 일을 수행하게 됩니다. 이 클래스에 대한 생성자는 WaitForConnection()을 실행할 새 스레드를 만드는 작업을 합니다. 이전 예제에서와 마찬가지로 소켓을 열고 연결을 수락합니다. 다음은 스레드에 대한 주요 루프입니다.

while (true)
{
    Console.WriteLine("Waiting for initial connection");
    listener.Start();
    Socket socket = listener.AcceptSocket();
    NetworkStream stream = new NetworkStream(socket);
    BinaryReader reader = new BinaryReader(stream);
    BinaryWriter writer = new BinaryWriter(stream);

    Console.WriteLine("Connection Requested");

    int userPort = port + 1;
    TcpListener specificListener;
    while (true)
    {
        try
        {
            specificListener = 
                new TcpListener(localAddr, userPort);
            specificListener.Start();
            break; 
        }
        catch (SocketException)
        {
            userPort++;
        }
    }
    // specificListener는 원격 사용자가
    // 사용해야 합니다. 이 포트를 원격 사용자에게 다시
    // 보낸 후 같은 포트에 서버를 만듭니다.
    SocketServer socketServer = new SocketServer(specificListener);

    writer.Write(userPort);
    writer.Close();
    reader.Close();
    stream.Close();
    socket.Close();
}

저는 다중 연결을 지원하고 싶기 때문에 클라이언트에서 연결이 필요하다는 것을 나타내기 위해서 여러 클라이언트에 대해 하나의 포트를 사용합니다. 그러면 서버는 사용 가능한 포트를 찾아 클라이언트에게 다시 보낼 것이며 이 포트는 해당 클라이언트에 대한 연결로 사용됩니다.

사용하지 않는 포트를 검색하는 방법을 아직 모르기 때문에 사용 가능한 포트를 찾을 때까지 while loop에서는 여러 포트를 사용해 봅니다. 그런 다음 포트 번호를 클라이언트에게 다시 돌려 보내고 포트를 정리합니다.

그런데 여기서 짚고 넘어가야 할 미묘한 문제가 있습니다. SocketServer의 원본 버전은 포트 번호를 매개 변수로 사용하여 해당 포트에서 수신기를 설정하기 전에 클라이언트가 먼저 요청을 할 수 있어서 좋지 않았습니다. 그러한 경우를 대비하여 클라이언트에게 포트 번호를 보내기 전에 TcpListener를 만들어서 Race Condition이 없다는 것을 확인합니다.

SocketServer 클래스는 추가 스레드를 만들며 다음 주요 루프를 사용합니다.

try
{
    while (true)
    {
        MessageType messageType = 
            (MessageType) reader.ReadInt32();

        switch (messageType)
        {
            case MessageType.RequestEmployee:
                Employee employee = 
                new Employee("Eric Gunnerson", "One Microsoft Way");
                employee.Send(writer);
                break; 
        }
    }
}
catch (IOException)
{

}
finally
{
    socket.Close();
}

이 주요 루프는 단순 요청 가져오기/요청 처리 루프입니다. try-catch-finally는 클라이언트 연결이 끊어졌을 때 발생하는 예외로부터 복구합니다.

클라이언트 쪽의 이벤트

클라이언트 쪽에서 PC 또는 Pocket PC에 대해 Windows Forms 클라이언트를 작성합니다. Windows Forms 환경은 이벤트 기반으로서 이벤트를 사용하여 소켓 메시지를 처리하는 것이 바람직합니다. 이 작업은 SocketClient 클래스를 통해 이루어집니다. 첫 번째 단계는 각 메시지에 대한 대리자 및 이벤트를 정의하는 것입니다.

        public delegate void EmployeeHandler(Employee employee);
        public event EmployeeHandler EmployeeReceived;

두 번째 단계는 코드를 작성하여 이벤트를 보내는 것입니다.

    case MessageType.Employee:
        Employee employee = new Employee(reader);
        if (EmployeeReceived != null)
            form.Invoke(EmployeeReceived, new object[] {employee});
        break; 

이벤트가 발생했을 때 폼을 업데이트해야 하는 경우도 있습니다. 이 작업을 안정적으로 실행하려면 주 UI 스레드에서 실행해야 합니다. 이 작업은 폼에서 Invoke() 호출을 통해 이루어지는데 이는 주 UI 스레드에서 대리자를 호출하도록 정렬하게 됩니다.

메시지 기반 아키텍처이기 때문에 서버에서 발생하는 비동기 이벤트에 대한 지원이 기본 제공됩니다. 예제는 서버가 매초 보내는 CurrentCount 메시지입니다. 예제 코드는 SocketFinal 하위 디렉터리에 있습니다.

요약

저는 이 소켓 기반 아키텍처에 대체적으로 만족합니다. 가볍고 사용하기도 쉬우며 PC와 Pocket PC에서 모두 실행할 수 있다는 점이 좋습니다.

다음 달

PDC  개최 시기가 다가오고 있습니다. 이번 PDC 기간 동안 참석자들에게 여러 기술 정보를 소개하게 됩니다. Whidbey가 완성되려면 아직도 많은 작업이 이루어져야 하기 때문에 기초적인 정보밖에 제공하지 못하지만 새 언어 기능에 대해서는 좀 더 자세히 다룰 것입니다. 그 내용에 대해서는 다음 달에 설명하도록 하겠습니다.


Eric Gunnerson은 Visual C# 팀의 프로그램 매니저로서 C# 언어 디자인 팀의 이전 및 현 회원으로 활동하고 있으며 A Programmer's Introduction to C#, 2nd Edition 의 저자입니다. 그는 매우 오랫동안 프로그래밍 작업을 해 왔기 때문에 8인치 디스켓에 대해 명확하게 파악하고 있으며 손쉽게 테이프를 탑재할 수 있습니다. 여가 시간에는 Roomba 를 지켜 보면서 소일하는 것을 좋아합니다.



화면 맨 위로화면 맨 위로


최종 수정일: 2003년 11월 25일
Top of Page Top of Page


Microsoft