익스플로러를 이용한 인터넷 프로그래밍 채팅 프로그램 만들기 지난 호에서는 넷미팅에 대한 간략한 개론을 살펴보고 이를 바탕으로 INmManager 인터페이스와 INmSysInfo 인터페이스를 사용해보는 아주 간단한 예제 프로그램을 만들어 보았다. 이번 호는 이를 보다 발전시켜 채팅 프로그램을 만들어 보도록 하자. 넷미팅을 이용한 프로그래밍은 크게 두 가지 종류로 나누어 볼 수 있다. 간단한 방법으로는 넷미팅에 제공되는 액티브X 컨트롤을 사용하는 방법이 있다. 이보다 복잡한 방법으로는 넷미팅 COM 컴포넌트를 사용하는 것이다. 후자의 방법은 상당히 난해한 편이다. 기본적으로 C++를 사용해야만 하고 COM에 대해 어느 정도의 지식이 있어야 한다. 넷미팅 COM 컴포넌트를 사용하는 프로그래밍에도 크게 두 가지 방법이 존재한다. 넷미팅 UI를 사용하는 방법과 넷미팅 UI를 사용하지 않는 방법이 있다. 지난 호에 만들어 본 예제와 마찬가지로 이번 호의 예제 프로그램 역시 C++를 이용해 넷미팅 COM 컴포넌트를 만든 다음 넷미팅 UI를 사용하지 않는 프로그램으로 만들어 볼 생각이다. 이번 호 예제 프로그램은 일대일 채팅 프로그램이다. 오디오나 비디오, 파일 전송, 프로그램 공유, 화이트 보드 등의 기능은 지원하지 않는다. 원래 넷미팅 자체는 멀티미디어 데이터의 송수신 프로토콜인 H.323을 컴포넌트화한 것이라고 볼 수 있는데 사실 채팅은 넷미팅의 데이터 채널을 이용하는 것으로 이는 T.120이라는 프로토콜을 기반으로 한다. 채팅 예제 프로그램 자체는 넷미팅 SDK에 딸려오는 NMUI라는 예제 프로그램(그림 1 참조)을 바탕으로 만들었다. NMUI 프로그램은 소스의 양이 상당히 방대하고 어렵다. 하지만 넷미팅의 기능이 거의 완벽하게 구현된 예제 프로그램이기 때문에 넷미팅 프로그래밍을 할 사람이라면 누구나 반드시 살펴보아야 할 프로그램이다. 이 프로그램은 소스의 모듈화가 잘 되어 있기 때문에 다른 프로그램에 가져다 쓰기에도 좋은 편이다. 넷미팅 컴포넌트 넷미팅은 두 사람간의 오디오, 비디오 연결만을 지원한다. 여러 사람(그룹)이 같이 오디오, 비디오 컨퍼런스를 갖고 싶다면 MCU(Multiple Control Unit)라는 것을 이용해야 한다. MCU로는 화이트 파인 소프트웨어(White Pine Software)의 미팅포인트(Meeting Point)라는 프로그램이 제일 유명하다. 넷미팅은 데이터 통신(텍스트 채팅, 공유 화이트보드, 프로그램 공유, 파일 전송)의 경우에 최대 8명까지의 연결을 지원한다. 동작 방식 넷미팅을 이용한 프로그래밍을 하려면 일단 넷미팅을 구성하는 컴포넌트들의 종류와 이들의 역할이 무엇인지 이해할 필요가 있다. 제일 중요한 넷미팅 컴포넌트는 바로 매니저 컴포넌트(INmManager)이다. 이를 이용해야만 다른 기능들을 사용할 수 있다. 매니저 컴포넌트를 만들면 이것으로 넷미팅 초기화는 완료된 것이나 다름없다. 다음으로 오디오/비디오/데이터 통신을 할 상대를 지정해야 한다. 이 때 사용되는 것이 콜 컴포넌트(INm Call)이다. 이를 이용해 상대와 연결이 이루어지면 이는 컨퍼런스 컴포넌트(INmConference)로 대표된다. 즉 콜이 성공하면 회의가 만들어지는 개념이라고 생각하면 된다. 컨퍼런스의 참여자들은 멤버 컴포넌트(INm Member)로 대표된다. 멤버들간의 오디오/비디오/일반 데이터의 송수신은 채널 컴포넌트(INmChannel)를 통해 이루어진다. 채널 컴포넌트에는 오디오 채널(INm ChannelAudio), 비디오 채널(INmChannelVideo), 데이터 채널(INmChannelData), 파일 전송 채널(INm ChannelFt), 프로그램 공유 채널(INmChannel App Share) 등 서브 채널 컴포넌트들이 존재한다. 이러한 컴포넌트들은 우리가 넷미팅에 어떤 명령을 내리기 위해 사용하는 것이고 넷미팅에 내린 명령에 대한 결과나 넷미팅에서 알려주는 어떤 이벤트를 받기 위한 알림 컴포넌트(Notification Component)들이 존재한다. 대부분의 넷미팅 컴포넌트들은 그에 대응하는 알림 컴포넌트를 갖고 있다. 이에 대해서는 지난 호에 언급한 바 있지만 다시 한번 정리하면 표 1과 같다. 왼편의 컴포넌트들은 넷미팅에 명령을 내리는데 사용되는 것으로 개발자가 만드는 것이 아니라 넷미팅에서 제공되는 것을 단지 가져다 사용하면 된다. 오른편의 컴포넌트들은 왼편의 컴포넌트에서 발생하는 일에 대해 보고받아 그에 대응하기 위한 컴포넌트로 개발자가 직접 만들어 넷미팅에 제공해야 하는 것이다. 즉 오른편의 컴포넌트는 개발자가 만들어야 한다. 예제 프로그램에서 그 예를 볼 수 있을 것이다. 넷미팅의 주요 컴포넌트들의 기능에 대해 다시 한번 간략히 정리하면 다음과 같다. · 매니저 컴포넌트(INmManager) : 넷미팅의 가장 핵심이 되는 컴포넌트로 다른 컴포넌트들은 모두 이 컴포넌트를 통해 만들게 된다. · 콜 컴포넌트(INmCall) : 넷미팅을 통해 대화를 나눌 양방을 연결해주는 역할을 한다. · 컨퍼런스 컴포넌트(INmConference) : 콜이 연결되어 대화를 나누고 있는 그룹을 나타내는 역할을 한다. · 멤버 컴포넌트(INmMember) : 컨퍼런스에 참여하고 있는 참여자를 나타낸다. · 채널 컴포넌트(INmChannel) : 컨퍼런스에 참여한 참여자들간에 오디오/비디오/일반 데이터를 주고 받는데 사용된다. 간단히 요약하면 콜을 연결하여 컨퍼런스를 만든 다음 채널을 통해 데이터를 주고받는다고 할 수 있다. 이 절차를 정리하면 다음과 같다. ① 넷미팅을 초기화(INmManager::Initialize)한다. 넷미팅을 초기화할 때 대부분의 경우에는 INmManagerNotify 객체를 하나 만들어 매니저 객체에 알려주어야 한다. 그래야만 외부에서 새로운 콜이 들어오거나 컨퍼런스가 발생하면 이를 보고받을 수 있다. ② 연결할 상대에게 콜(Call)을 전송(INmManager::CreateCall)한다. 이미 만들어져 있는 컨퍼런스에 참여하고 싶다면 INmManager:: CallConference를 사용한다. 콜 객체는 콜을 받는 상대방에도 하나 만들어진다. 이는 상대방 프로그램에서 직접 만드는 것이 아니라 넷미팅에서 만들어서 INmManagerNotify::Call Created로 이벤트로 넘긴다. 이 이벤트는 콜을 받는 쪽뿐만 아니라 콜을 만들어 보낸 쪽에도 발생한다. 여기서 콜에 대한 진행 상황을 알고 싶다면 INmCallNotify 객체를 하나 만들어 콜 객체 쪽에 알려 주어야 한다. 그러면 콜이 상대방에 의해 수락되었는지 아니면 거부되었는지 등의 진행 상황을 보고받을 수 있다. 이는 INmCallNotify::StateChan ged 이벤트로 알 수 있다. ③ 상대방이 콜을 수락하면 컨퍼런스가 하나 생성된다. 상대방이 콜의 수락하는 곳은 INmCallNotify::StateChanged의 인자로 NM_CALL_RING이 들어왔을 경우이다. 콜의 수락은 INmCall::Accept 메소드를 호출하면 된다. 콜이 수락되면 콜을 생성한 쪽에는 INmCallNotify::StateChanged의 인자로 NM_CALL_ ACCEPTED가 넘어가고 콜을 수락한 쪽에는 INmCallNotify:: Accepted으로도 수락 여부가 알려진다. 사실 컨퍼런스는 이보다 먼저 만들어지지만 이 컨퍼런스가 인정되는 것은 콜이 양방향에서 허락된 다음이다. 컨퍼런스는 넷미팅에서 만들어서 넘겨주는데 이는 INmManagerNotify::ConferenceCreated 이벤트로 알려진다. 앞서 콜 객체나 매니저 객체에서처럼 컨퍼런스 객체에서 발생하는 일에 대해 보고받고 싶다면 INmConfNotify 객체를 만들어 이를 컨퍼런스 객체에 알려야 한다. INmManagerNotify::ConferenceCreated 이벤트에서 INmConfNotify 객체를 만들어 이를 컨퍼런스 객체에 알린다. ④ 컨퍼런스 내의 채널과 멤버가 정해진다. 컨퍼런스가 만들어지면 여기에 참여할 멤버가 결정되어야 한다. 이는 INmConfNotify::MemberChanged 이벤트로 알려진다. 이와 함께 컨퍼런스 내에서 사용할 채널이 결정되어야 한다. 이 채널은 INmConfNotify::ChannelChanged 이벤트로 알려진다. 앞서 살펴본 것과 같이 채널에는 여러 종류가 존재하는데 넷미팅에서 양방간에 연결이 가능한 채널 타입을 살펴보고 이 채널들에 대해서만 INmConfNotify::ChannelChanged 이벤트를 전송한다. 예를 들어 어떤 시스템에는 오디오 기능이 있는데 다른 시스템에는 오디오 기능이 없다면 INmChannelNotify::ChannelChanged 이벤트로 오디오는 발생하지 않는다. 예제 프로그램에서는 이 채널들 중에서 INmChannelData만을 사용한다. 이 채널은 일반 데이터를 주고받기 위한 채널로 이를 이용해 여러 가지 작업을 수행할 수 있는데 그 중에서 가장 대표적인 것이 바로 채팅이라고 할 수 있다. 이 데이터 채널을 통해 데이터를 받거나 자신이 보낸 데이터가 무엇인지 알고 싶다면 다른 넷미팅 컴포넌트에서와 마찬가지로 INmChannel DataNotify 객체를 알림 객체로 연결해 주어야 한다. ⑤ 사용자간에 채팅 데이터를 주고 받는다. 채팅 데이터는 채팅 컴포넌트(INmChannelData)의 SendData로 전송할 수 있다. 데이터를 보낼 상대는 INmMember 인터페이스로 지정하면 된다. 제대로 전송되면 INmChannelData::DataSent로 그 데이터가 다시 넘어온다. 상대가 나에게로 보낸 데이터는 INmChannelData::DataReceived로 받아볼 수 있다. 이것으로 간략히 동작 절차를 알아보았다. 간략히 알아보긴 했지만 위의 절차는 아주 중요하다. 위의 절차를 모르고 예제 프로그램의 소스를 보면 그 내용을 알기 힘들다. 이제 이 연재에서 가장 중요한 넷미팅 인터페이스들에 대해 알아보도록 하자. 넷미팅 인터페이스 이해하기 지난 호에서는 매니저 컴포넌트와 시스템 정보 컴포넌트 인터페이스에 대해서만 살펴보았다. 이번에는 지난 호에서 알아보지 못한 매니저 컴포넌트의 알림 인터페이스(INmManagerNotify)와 콜 인터페이스(INmCall), 콜 알림 인터페이스(INmCallNotify), 컨퍼런스 인터페이스(INmConference)와 컨퍼런스 알림 인터페이스(INmConferenceNotify), 채널 인터페이스(INmChan nel), 채널 알림 인터페이스(INmChannelNotify) 들에 대해 간략하게 알아보자. ① 매니저 알림 인터페이스(INmManagerNotify) 넷미팅의 모든 알림 인터페이스는 ICNotify 인터페이스로부터 계승된다. ICNotify 인터페이스도 COM 인터페이스이기 때문에 당연히 IUnknown으로부터 계승된다. ICNotify 인터페이스에는 Connect와 Disconnect 메소드가 포함되어 있다. 이 메소드들은 알림 인터페이스와 알림의 대상간을 연결해주고 연결을 끊는 역할을 담당한다. 예를 들어 INmManagerNotify 인터페이스를 하나 만들고 그것의 Connect 메소드의 인자로 매니저 객체를 하나 지정해주면 그 매니저에 새로운 콜이나 컨퍼런스가 생성될 때마다 해당 INmManagerNotify 인터페이스의 메소드들이 호출된다. 아무튼 넷미팅의 모든 알림 인터페이스에는 Connect, Disconnect, QueryInter face나 AddRef, Release 등이 당연히 포함된다. 이들 메소드에 대한 설명은 제외하도록 하겠다. INmMana gerNotify는 표 2에 나타난 메소드로 구성된다. 알림 인터페이스이므로 개발자가 자신이 필요로 하는 용도에 맞게 만들어 제공해야 한다는 점에 주의하기 바란다. ② 콜 인터페이스(INmCall) 들어온 콜을 수락하거나 거부하고 요청한 콜을 취소하는 등의 작업을 하는데 사용된다. 이것은 넷미팅에서 제공되는 인터페이스이기 때문에 그냥 사용하면 된다(표 3 참조). ③ 콜 알림 인터페이스(INmCallNotify) 콜 알림 인터페이스는 콜 관련 이벤트를 넷미팅에서 프로그램 쪽으로 알리는데 사용된다. StateChanged와 Accepted를 주의깊게 살펴보기 바란다. 알림 인터페이스이므로 이 인터페이스는 개발자가 직접 만들어야 한다(표 4 참조). ④ 컨퍼런스 인터페이스(INmConference) 콜이 성공하면 컨퍼런스가 만들어진다. 이러한 컨퍼런스를 제어하는데 표 5와 같은 메소드를 사용할 수 있다. ⑤ 컨퍼런스 알림 인터페이스(INmConferenceNotify) 컨퍼런스 알림 인터페이스는 컨퍼런스 관련 이벤트를 넷미팅에서 프로그램 쪽에 알리는데 사용된다. State Changed와 Accepted를 주의깊게 보면 된다. 알림 인터페이스이기 때문에 이 인터페이스는 만들어야 한다(표 6 참조). ⑥ 채널 인터페이스(INmChannel) 넷미팅 채널을 조작하고자 할 때 쓰이는 인터페이스로 이 인터페이스로부터 데이터 채널 인터페이스와 같은 기타 채널 인터페이스들이 계승되었다(표 7 참조). ⑦ 채널 알림 인터페이스(INmChannelNotify) 채널 알림 인터페이스는 넷미팅에서 채널 관련 이벤트를 프로그램 쪽에 알리는데 사용된다. StateChanged와 Accepted를 주의 깊게 보면 된다. 알림 인터페이스이기 때문에 이 인터페이스는 만들어야 한다(표 8 참조). ⑧ 데이터 채널 인터페이스(INmChannelData) 여러 채널 타입 중에서 예제 프로그램에서 사용해볼 것은 데이터 채널이다. 이것은 INmChannel로부터 계승되었으므로 앞에서 살펴본 INmChannel이 갖고 있는 메소드 이외에 표 9와 같은 메소드를 갖는다. ⑨ 데이터 채널 알림 인터페이스(INmChannelDataNotify) 데이터 채널에 상대가 보낸 데이터가 도착하거나 자신의 보낸 데이터가 출력 큐에 쌓이면 다음과 같은 이벤트를 발생시킨다(표 10 참조). 이외에도 많은 채널 관련 인터페이스들이 있는데 그 것들에 대해서는 지면 관계상 더 이상 설명하지 않도록 하겠다. 마지막으로 INmMember 인터페이스에 대해 살펴보자. ⑩ 멤버 인터페이스(INmMember) 컨퍼런스에 참여한 멤버에 대한 작업을 할 수 있는 표 11의 메소드를 제공한다. 예제 프로그램의 작성 예제 프로그램은 지난 회와 마찬가지로 MFC로 작성하였다. 프로젝트 타입은 다이얼로그 기반으로 선택하였고 이름은 Chat로 정했다. 예제 프로그램은 앞서 언급한 것처럼 일대일 채팅 프로그램이다. 실행 화면은 그림 2와 같다. 우측의 리스트 박스는 일종의 디버깅용 로그창으로 작업의 진행 상황을 표시해준다. 상대와 연결하고자 할 때 “상대 연결” 버튼을 누르면 그림 3과 같은 다이얼로그가 떠서 상대방의 IP 주소를 입력받을 수 있다. 이 예제 프로그램을 컴파일하려면 넷미팅 SDK가 있어야 한다. 참고로 넷미팅 SDK는 마이크로소프트 웹 사이트(http://www.microsoft.com/windows/Net Meeting/Authors/SDK/Default.ASP)에서 다운로드받을 수 있다. 프로그래밍시에 넷미팅 SDK의 API 도움말을 참고할 일이 많을 것이다. 넷미팅 SDK를 다운받아 설치했으면 Include 디렉토리의 imsconf2.idl을 예제 프로젝트의 디렉토리로 복사한다. 그 다음에 이를 프로젝트에 추가하고 이 프로젝트 세팅을 변경해야 한다. 절차가 좀 복잡한 편인데 이에 대해서는 지난 회 기사의 “예제 프로그램-넷미팅 시스템 정보 알아내기”를 참고하기 바란다. 이제 넷미팅 관련 코드를 살펴보기로 하자. 상세한 코드는 예제 프로그램의 소스를 다운받아 보기 바라며 여기서는 앞서 설명했던 넷미팅의 동작 방식에 의거해서 중요한 부분만 설명하도록 하겠다. 넷미팅 초기화 넷미팅 초기화는 CChatDlg의 OnCreate 함수에서 수행하였다. InitConfMgr이라는 멤버 함수를 호출하여 실행했으며 이 함수의 내용은 리스트 1과 같다. 리스트 1의 소스를 보면 매니저 객체를 만든 후 매니저의 알림 객체(CMgrNotify)를 만들어 연결하고 있다. 즉, 매니저 객체에 어떤 일(콜이 생겼다든가 컨퍼런스가 발생했다든가)이 발생하면 이를 연락받기 위한 것이다. CMgrNotify 클래스가 바로 INmManagerNotify 인터페이스를 클래스화한 것이다. 앞서 설명한 것처럼 알림 인터페이스가 개발자가 직접 만들어서 넷미팅에 제공해주어야 한다. 그 부분의 코드는 다음과 같다. hr = m_pMgrNotify->Connect(m_pMgr); AddDebugMessage라는 함수도 볼 수 있는데 이 함수는 앞서 그림 2의 우측 리스트 박스에 한 줄을 표시하는 역할을 하는 함수이다. 그러면 CMgrNotify 클래스의 실제 소스는 뒤에서 다시 살펴보기로 하고 다음으로 콜을 전송하는 부분에 대해 살펴보자. 리스트 1. 넷미팅 초기화 HRESULT CChatDlg::InitConfMgr(BOOL bUseNMUI) { HRESULT hr; LPCLASSFACTORY pcf; // 컨퍼런스 매니저 객체를 생성한다. hr = CoGetClassObject(CLSID_NmManager, CLSCTX_INPROC, NULL, IID_IClassFactory, (void**)&pcf); if (FAILED(hr)) { AddDebugMessage(“컨퍼런스 매니저 객체 생성에 실패했습니다.”); return hr; } else { // IID_INmManager 인터페이스에 대한 포인터를 얻는다. hr = pcf->CreateInstance(NULL, IID_INmManager, (void**)&m_pMgr); if (FAILED(hr)) AddDebugMessage(“INmManager::CreateInstance에 실패했 습니다.”); else { // 컨퍼런스 매니저 객체에 연결할 이벤트 객체를 만든다. m_pMgrNotify = new CMgrNotify(); if (m_pMgrNotify == NULL) { hr = E_OUTOFMEMORY; AddDebugMessage(“CMgrNotify를 생성할 수 없습니다.”); } else { // 컨퍼런스 매니저 객체와 이벤트 객체를 연결한다. hr = m_pMgrNotify->Connect(m_pMgr); if (FAILED(hr)) { AddDebugMessage(“CMgrNotify::Connect 실패 했습니다.”); m_pMgrNotify->Release(); m_pMgrNotify = NULL; } else { // Nm을 초기화한다. ULONG uchCaps = NMCH_ALL; ULONG uOptions; if (bUseNMUI) uOptions = (ULONG)NM_INIT_NORMAL; else uOptions = (ULONG)NM_INIT_CONTROL; hr = m_pMgr->Initialize(&uOptions, &uchCaps); } } } pcf->Release(); } return hr; } 콜의 생성 콜을 생성하는데는 INmManager::CreateCall 메소드를 사용한다. 인자로 콜 요청을 받을 상대의 주소(IP 주소)가 필요하다. 이를 입력하기 위한 다이얼로그 클래스가 바로 그림 3의 CGetIP 클래스이다. 이 다이얼로그 클래스는 너무 간단해서 소스를 보면 금방 이해할 수 있을 것이다. 이 다이얼로그를 띄워 주소를 입력받고 콜을 전송하는 부분의 코드는 다음과 같다. void CChatDlg::OnConnect() { CGetIP getIPDlg(this); if (getIPDlg.DoModal() == IDOK) { HRESULT hr; INmCall * pCall; CString strIP; strIP = getIPDlg.m_strIP; // getIPDlg.m_strIP로 연결을 시도한다. if (NULL == m_pMgr) return; // IP 주소를 유니코드로 변경한다. BSTR bstrAddr = strIP.AllocSysString(); hr = m_pMgr->CreateCall(&pCall, NM_CALL_T120, NM_ADDR_IP, bstrAddr, NULL); if (SUCCEEDED(hr)) { pCall->Release(); } } } INmManager::CreateCall의 인자에 대한 자세한 설명은 넷미팅 SDK를 참고하기 바란다. 두 번째 인자가 콜의 타입을 지정하는데 데이터 통신만을 할 것이기 때문에 NM_CALL_T120으로 지정했다. 세 번째 인자는 상대 주소의 지정 방식을 나타내는데 NM_ADDR_IP를 할당하면 말 그대로 상대 주소를 IP 주소로 지정하겠다는 것이다. 네 번째 인자로 상대의 IP 주소를 유니코드로 지정한 것을 볼 수 있다. 콜이 만들어져서 상대방과 연결되면 양쪽 모두에 콜 객체가 생성된다. 생성된 콜 객체는 INmManagerNotify의 CallCreated 메소드로 리턴된다. 그러면 여기서 다시 이 알림 인터페이스에 대해 살펴보기로 하자. 매니저 컴포넌트 뿐만 아니라 콜, 채널, 컨퍼런스 모두 각기 알림 인터페이스를 갖는데 구성은 모두 유사하다. 매니저 컴포넌트의 알림 인터페이스 구성만 이해한다면 다른 것들도 이해한 셈이다. 다음의 CMgrNotify 클래스는 INmManagerNotify 인터페이스를 클래스화한 것이다. class CMgrNotify : public INmManagerNotify { DWORD m_dwCookie; // 매니저와 매니저 알림 인터페이스간의 연결을 나타내는 값 IUnknown * m_pUnk; IConnectionPoint *m_pcnp; IConnectionPointContainer *m_pcnpcnt; DWORD m_dwObjRefCount; public: CMgrNotify(); ~CMgrNotify(); // INmManagerNotify HRESULT STDMETHODCALLTYPE NmUI(CONFN uNotify); HRESULT STDMETHODCALLTYPE ConferenceCreated(INmConference *pConference); HRESULT STDMETHODCALLTYPE CallCreated(INmCall *pCall); // ICNotify methods HRESULT STDMETHODCALLTYPE Connect (IUnknown *pUnk); HRESULT STDMETHODCALLTYPE Disconnect(void); // IUnknown methods ULONG STDMETHODCALLTYPE AddRef(void); ULONG STDMETHODCALLTYPE Release(void); HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, PVOID *ppvObj); IUnknown * GetPunk() {return m_pUnk;} }; 위의 소스를 보면 IConnectionPoint와 IConnection PointContainer라는 인터페이스가 보이는데 이는 인터페이스간에 이벤트를 받는데 사용되는 인터페이스들로 잠깐 설명하기에는 복잡하다. 요즘 필자가 구루 칼럼에 연재 중인 COM 심층 분석에서 기회가 된다면 자세히 다뤄볼 생각이다. 여기서는 인터페이스간에 이벤트를 받는데 사용된다는 정도로만 설명을 끝내겠다. CMgrNotify 클래스의 구현 코드 중에서 앞서 진행하던 설명에 필요한 CallCreated의 코드는 다음과 같다. 콜이 하나 생성되었으므로 이를 CChatDlg의 m_ listCall 리스트에 추가하고 이 콜로부터 발생하는 이벤트를 보고받기 위해 CCallNotify 클래스의 객체를 하나 만들어 이를 연결하였다. 앞서 매니저 객체의 초기화 작업에서와 흡사한 순서를 밟고 있다. 이렇게 만들어진 CCall Notify 객체는 CChatDlg의 m_listCallNotify 리스트에 추가된다. CCallNotify 클래스는 INmCallNotify 인터페이스를 구현한 것으로 여기에는 콜의 상태를 알려주는 StateChanged라는 이벤트가 있다. 코드는 리스트 3과 같다. 리스트 3을 보면 인자로 넘어오는 값에 따라 일을 처리하고 있다. NM_CALL_RING이 들어오고 그 콜의 IsIncoming 메소드의 리턴값이 S_OK이면 이는 어떤 상대로부터 콜 요청이 들어온 것이다. 그러면 메시지 박스를 하나 띄워 대화를 나눌 것인지 물어본다. ‘예’를 선택하면 콜의 Accept 메소드를 호출하여 콜 요청을 받아들이고 ‘아니오’를 선택하면 Reject 메소드를 호출하여 콜 요청을 거부한다. 콜 요청이 수락되면 StateChanged 메소드의 인자로 NM_CALL_ACCEPTED가 들어오고 이 때 채팅 관련 컨트롤들의 상태를 활성화시킨다. 콜 요청이 거부되면 NM_CALL_REJECTED가 들어온다. 리스트 2 : CMgrNotify의 CallCreated 메소드 // ------------------------------------------- // 새로운 콜이 하나 생기면 이 함수가 호출된다. // ------------------------------------------- HRESULT STDMETHODCALLTYPE CMgrNotify::CallCreated(INmCall *pCall) { HRESULT hr; CCallNotify *pNotify; CChatDlg *pDlg = (CChatDlg *)AfxGetMainWnd(); BYTE* ppb=NULL; ULONG pcb=0; pDlg->AddDebugMessage(“Call이 하나 생성되었습니다.”); pCall->AddRef(); pDlg->m_listCall.AddTail(pCall); pNotify = new CCallNotify(); if (pNotify == NULL) { pDlg->AddDebugMessage(“CCallNotify를 생성할 수 없습니다.”); hr = E_OUTOFMEMORY; } else { hr = pNotify->Connect(pCall); if (FAILED(hr)) { pDlg->AddDebugMessage(“CMgrNotify::Connect에 실패했습니다.”); pNotify->Release(); } else { pDlg->AddDebugMessage(“CCallNotify를 생성하였습니다.”); pDlg->m_listCallNotify.AddTail(pNotify); } } return hr; } 컨퍼런스의 생성 콜이 연결되고 수락되면 이 때부터 컨퍼런스가 정식으로 동작한다. 사실 콜이 만들어지는 시점부터 컨퍼런스는 만들어져 있다. 처음에는 휴지(idle) 상태이다가 컨퍼런스에 구성원들이 들어오면 활성화 상태로 변경된다. 컨퍼런스가 만들어지면 이는 매니저 알림 인터페이스의 Con ferenceCreated 메소드로 통보된다(리스트 4 참조). 앞서 많이 보았던 코드에서처럼 컨퍼런스에서도 알림 객체를 하나 만들어 연결하고 있다. 컨퍼런스 알림 객체로 컨퍼런스 내에 새로 추가되는 멤버와 채널에 대한 정보가 통보된다. 리스트 3 : CCallNotify::StateChanged 메소드 HRESULT STDMETHODCALLTYPE CCallNotify::StateChanged (NM_CALL_STATE uState) { HRESULT hr; CChatDlg *pDlg = (CChatDlg *)AfxGetMainWnd(); if (uState == NM_CALL_RING) // 외부에서 들어온 콜의 경우에만 다이얼로그를 띄운다. { pDlg->AddDebugMessage(“CCallNotify::StateChanged 외부에서 전화가 오고 있습니다.”); INmCall *pCall = (INmCall *)m_pUnk; if (pCall->IsIncoming() == S_OK) { BSTR bstrName; pCall->GetName(&bstrName); CString str = bstrName; CString strMsg; pDlg->AddDebugMessage(“CCallNotify::State Changed 외부에서 온 콜입니다.”); strMsg.Format(“%s님으로부터 온 채팅 요청을 수락하시겠습니까 ?”, str); UINT nRet = AfxMessageBox(strMsg, MB_YESNO); // 콜을 받을 것인지 여부를 결정한다. if (nRet == IDYES) hr = pCall->Accept(); else hr = pCall->Reject(); } } else if (uState == NM_CALL_ACCEPTED) { pDlg->AddDebugMessage(“CCallNotify::StateChanged 콜이 수락되었습니다.”); // 이 경우에 버튼들을 활성화한다. AfxGetMainWnd()->GetDlgItem(IDC_LOG)->EnableWindow(TRUE); AfxGetMainWnd()->GetDlgItem(IDC_INPUT)->EnableWindow(TRUE); AfxGetMainWnd()->GetDlgItem(IDC_SEND)->EnableWindow(TRUE); } else if (uState == NM_CALL_REJECTED) { pDlg->AddDebugMessage(“CCallNotify::StateChanged 콜이 거부되었습니다.”); AfxMessageBox(“호출이 거부되었습니다.”); } return S_OK; } 리스트 4 : CMgrNotify 클래스의 ConferenceCreated의 구현 HRESULT STDMETHODCALLTYPE CMgrNotify::ConferenceCreated (INmConference *pConference) { HRESULT hr; CConfNotify * pNotify; CChatDlg *pDlg = (CChatDlg *)AfxGetMainWnd(); pDlg->AddDebugMessage(“Conference가 하나 생성되었습니다.”); pConference->AddRef(); pDlg->m_listConference.AddTail(pConference); pNotify = new CConfNotify(); if (NULL == pNotify) { hr = E_OUTOFMEMORY; pDlg->AddDebugMessage(“CConfNotify를 생성할 수 없습니다.”); } else { hr = pNotify->Connect(pConference); if (FAILED(hr)) { pDlg->AddDebugMessage(“CConfNotify::Connect에 실패했습니다.”); pNotify->Release(); pNotify = NULL; } else { pDlg->AddDebugMessage(“CConfNotify 객체를 성공적으로 만들었습니다”); pDlg->m_listConferenceNotify.AddTail(pNotify); } } return S_OK; } 컨퍼런스 내의 채널과 멤버 추가/삭제 컨퍼런스 알림 객체는 CConfNotify라는 클래스로 구현된다. 이 클래스는 INmConferenceNotify 인터페이스를 구현한 것이다. 이 메소드 중 StateChanged로 컨퍼런스의 상태 변화가 알려지고 MemberChanged로 컨퍼런스의 참여자 추가/삭제 정보가 알려진다. Channel Changed로 컨퍼런스에서 사용할 채널의 추가/삭제/변경 정보가 통보된다. 이 중에서 Channel Changed의 내용 중 채널을 추가하는 부분은 다음과 같다. 컨퍼런스에 새로운 사용자가 들어오면 그 사용자와 컨퍼런스내의 사용자들간에는 채널이 형성된다. 형성되는 채널의 종류는 두 사용자의 시스템 상태에 따라 다르다. 리스트 5에서는 데이터 채널일 경우에만 INmChan nelData 인터페이스를 뽑아내고 알림 객체(CData Notify)를 연결하고 있다. CDataNotify 클래스는 INmChannelDataNotify 인터페이스를 클래스화한 것으로 이것의 DataSent와 DataReceived 메소드를 통해 송수신되는 텍스트 데이터를 알 수 있다. 텍스트 데이터의 송수신 데이터의 송신은 INmChannelData의 SendData 메소드를 통해 가능하다. 상대가 보낸 데이터의 수신은 INmChannelDataNotify의 DataReceived 메소드를 통해 통보받게 되며 내가 상대에게 보낸 데이터는 출력 큐에 놓인 다음 INmChannelDataNotify의 DataSent 메소드를 통해 통보받는다. DataReceived 부분의 코드는 다음과 같다. HRESULT STDMETHODCALLTYPE CDataNotify::DataReceived(INmMember *pMember, ULONG uSize, LPBYTE pb, ULONG dwFlags) { CChatDlg *pDlg = (CChatDlg *)AfxGetMainWnd(); LPTSTR psz; psz = (LPTSTR)pb; pDlg->AddDebugMessage(psz); if (dwFlags & NM_DF_SEGMENT_BEGIN) { pDlg->AddChatMessage(pMember, psz); // 채팅창에 메시지를 추 가한다. } return S_OK; } 이것으로 넷미팅 예제 프로그램을 살펴보았다. 사실 방대한 내용이라 1회 분량으로 설명하기엔 좀 벅찬 감이 없지 않았다. 소스를 참조하며 열심히 공부하기 바란다. INmConferenceNotify의 ChannelChanged 메소드의 처리를 조금만 수정하면 오디오나 비디오 채널의 추가도 어렵지 않다. 넷미팅의 NMUI 예제의 해당 부분을 참고하기 바란다. 지난 6개월에 걸친 인터넷 프로그래밍의 연재도 이것으로 끝났다. 새로운 내용을 많이 다루려 했는데 어떤 부분에 있어서는 생각보다 충실하게 다루지는 못한 것 같다. 아무튼 많은 독자 여러분께 도움이 많이 많이 되었으면 하는 바램이다. 그 동안 연재되었던 내용에 대해 질문이 있으면 언제든지 주저말고 필자의 이메일로 보내 주기 바란다. 더 좋은 내용으로 다시 독자 여러분과 만날 것을 약속드린다. 리스트 5 : 컨퍼런스 채널의 추가/삭제 HRESULT STDMETHODCALLTYPE CConfNotify::ChannelChanged (NM_CHANNEL_NOTIFY uNotify, INmChannel *pChannel) { CChatDlg *pDlg = (CChatDlg *)AfxGetMainWnd(); HRESULT hr; switch (uNotify) { case NM_CHANNEL_ADDED: { CChannelNotify * pNotify; ULONG uch; pDlg->AddDebugMessage(“CConfNtfy::ChannelChanged CHANNEL ADDED”); pChannel->GetNmch(&uch); if (uch != NMCH_DATA) // 추가된 채널이 데이터 채널이 아닌 경우에는 무시한다. { pDlg->AddDebugMessage(“CConfNotify::ChannelChanged No Data channel”); break; } else pDlg->AddDebugMessage(“CConfNotify::ChannelChanged Data channel”); pChannel->AddRef(); pDlg->m_listChannel.AddTail(pChannel); pNotify = new CChannelNotify(); if (pNotify == NULL) { pDlg->AddDebugMessage(“CConfNotify::ChannelChanged” “ CChannelNotify out of memory”); hr = E_OUTOFMEMORY; } else { hr = pNotify->Connect(pChannel); .......... hr = pChannel->QueryInterface(IID_INmChannelData, (void **)&pDlg->m_pChannelData); .......... CDataNotify * pDataNotify; pDataNotify = new CDataNotify(); .......... hr = pDataNotify->Connect(pDlg->m_pChannelData); .........