다음을 통해 공유


그룹 채팅 애플리케이션 만들기

이 항목에서는 피어 그룹화 API를 사용하여 채팅 애플리케이션을 개발하는 주요 단계에 대한 관련 코드 샘플을 제공하고 각 API 호출에 대한 컨텍스트를 제공합니다. UI 동작 및 전체 애플리케이션 구조는 포함되지 않습니다.

참고

전체 피어 그룹 채팅 샘플 애플리케이션은 피어 SDK에 제공됩니다. 이 항목에서는 샘플 내에서 제공되는 함수를 참조합니다.

 

그룹 초기화

채팅 애플리케이션을 생성할 때 첫 번째 단계는 가장 높은 지원되는 버전으로 PeerGroupStartup 을 호출하여 피어 그룹화 인프라를 초기화하는 것입니다. 이 경우 PEER_GROUP_VERSION 애플리케이션 헤더 파일에 정의됩니다. 인프라에서 실제로 지원되는 버전은 peerVersion에서 반환됩니다.

    PEER_VERSION_DATA peerVersion;

    hr = PeerGroupStartup(PEER_GROUP_VERSION, &peerVersion);
    if (FAILED(hr))
    {
        return hr;
    }

그룹 만들기

가입할 수 있는 그룹이 없거나 애플리케이션 사용자가 새 그룹을 만들려는 경우 채팅 애플리케이션에서 피어 그룹을 만들 수 있어야 합니다. 이렇게 하려면 PEER_GROUP_PROPERTIES 구조를 만들고 피어 그룹의 분류자, 식별자 이름, 작성자의 피어 이름 및 현재 상태 수명을 포함하여 그룹에 대한 초기 설정으로 채워야 합니다. 이 구조체가 채워지면 PeerGroupCreate에 전달합니다.

//-----------------------------------------------------------------------------
// Function: CreateGroup
//
// Purpose:  Creates a new group with the friendly name.
//
// Returns:  HRESULT
//
HRESULT CreateGroup(PCWSTR pwzName, PCWSTR pwzIdentity)
{
    HRESULT hr = S_OK;
    PEER_GROUP_PROPERTIES props = {0};

    if (SUCCEEDED(hr))
    {
        if ((NULL == pwzName) || (0 == *pwzName))
        {
            hr = E_INVALIDARG;
            DisplayHrError(L"Please enter a group name.", hr);
        }
    }

    if (SUCCEEDED(hr))
    {
        CleanupGroup( );

        props.dwSize = sizeof(props);
        props.pwzClassifier = L"SampleChatGroup";
        props.pwzFriendlyName = (PWSTR) pwzName;
        props.pwzCreatorPeerName = (PWSTR) pwzIdentity;

        hr = PeerGroupCreate(&props, &g_hGroup);
        if (FAILED(hr))
        {
            DisplayHrError(L"Failed to create a new group.", hr);
        }
    }

    if (SUCCEEDED(hr))
    {
        hr = PrepareToChat( );
    }

    return hr;
}

초대 발급

초대를 발급할 때 초대 대상자의 GMC를 가져와야 합니다. 이러한 값은 초대 대상자의 ID 이름으로 PeerIdentityGetXML 을 호출하여 가져올 수 있습니다. 성공하면 ID 정보(RSA 공개 키가 있는 base-64로 인코딩된 인증서가 포함된 XML)가 초대자에서 가져온 후 초대를 만드는 데 사용할 수 있는 외부 위치(이 샘플의 파일)에 기록됩니다.

이 시점에서 초대받은 사용자에게 초대를 전달하기 위한 메커니즘을 설정해야 합니다. 메일 또는 파일 교환의 다른 보안 방법일 수 있습니다. 아래 샘플에서는 초대를 초대자의 컴퓨터로 전송할 수 있는 파일에 기록됩니다.

//-----------------------------------------------------------------------------
// Function: SaveIdentityInfo
//
// Purpose:  Saves the information for an identity to a file.
//           Displays a message if there was an error.
//
// Returns:  HRESULT
//
HRESULT SaveIdentityInfo(PCWSTR pwzIdentity, PCWSTR pwzFile)
{
    PWSTR pwzXML = NULL;
    HRESULT hr = PeerIdentityGetXML(pwzIdentity, &pwzXML);

    if (FAILED(hr))
    {
        DisplayHrError(L"Unable to retrieve the XML data for the identity.", hr);
    }
    else
    {
        FILE *fp = NULL;
        errno_t err = 0;

        err = _wfopen_s(&fp, pwzFile, L"wb");
        if (err != 0)
        {
            hr = E_FAIL;
            DisplayHrError(L"Please choose a valid path", hr);
        }
        else
        {
            if (fputws(pwzXML, fp) == WEOF)
            {
                hr = E_FAIL;
                DisplayHrError(L"End of file error.", hr);
            }
            fclose(fp);
        }

        PeerFreeData(pwzXML);
    }

    return hr;
}

ID와 같은 초대도 외부에서 발급됩니다. 이 예제에서 초대는 fputws 가 있는 파일에 기록됩니다. 여기서 초대 대상은 이를 가져와 PeerGroupJoin을 호출할 때 사용할 수 있습니다.

//-----------------------------------------------------------------------------
// Function: CreateInvitation
//
// Purpose:  Creates an invitation file for an identity.
//           Displays a message if there was an error.
//
// Returns:  HRESULT
//
HRESULT CreateInvitation(PCWSTR wzIdentityInfoPath, PCWSTR wzInvitationPath)
{
    HRESULT hr = S_OK;
    WCHAR wzIdentityInfo[MAX_INVITATION] = {0};
    PWSTR pwzInvitation = NULL;
    errno_t  err  = 0;
    FILE *file = NULL;
        
    err = _wfopen_s(&file, wzIdentityInfoPath, L"rb");
    if (err != 0)
    {
        hr = E_FAIL;
        DisplayHrError(L"Please choose a valid path to the identity information file.", hr);
    }
    else
    {
        fread(wzIdentityInfo, sizeof(WCHAR), MAX_INVITATION, file);
        if (ferror(file))
        {
            hr = E_FAIL;
            DisplayHrError(L"File read error occurred.", hr);
        }
        fclose(file);
    }

    if (SUCCEEDED(hr))
    {
        ULONGLONG ulExpire; // adjust time using this structure
        GetSystemTimeAsFileTime((FILETIME *)&ulExpire);

        // 15days in 100 nanoseconds resolution
        ulExpire += ((ULONGLONG) (60 * 60 * 24 * 15)) * ((ULONGLONG)1000*1000*10);

        hr = PeerGroupCreateInvitation(g_hGroup, wzIdentityInfo, (FILETIME*)&ulExpire, 1, (PEER_ROLE_ID*) &PEER_GROUP_ROLE_MEMBER, &pwzInvitation);
        if (FAILED(hr))
        {
            DisplayHrError(L"Failed to create the invitation.", hr);
        }
    }

    if (SUCCEEDED(hr))
    {
        err = _wfopen_s(&file, wzInvitationPath, L"wb");
        if (err != 0)
        {
            hr = E_FAIL;
            DisplayHrError(L"Please choose a valid path to the invitation file.", hr);
        }
        else
        {
            if (fputws(pwzInvitation, file) == WEOF)
            {
                hr = E_FAIL;
                DisplayHrError(L"End of file error.", hr);
            }
            fclose(file);
        }
    }

    PeerFreeData(pwzInvitation);
    return hr;
}

피어 그룹 가입

피어가 다른 피어에서 만든 피어 그룹에 조인하려는 경우 해당 피어의 초대가 필요합니다. 초대는 외부 프로세스 또는 애플리케이션에서 초대받은 사용자에게 전달되고 아래 샘플에서 pwzFileName으로 지정된 로컬 파일에 저장됩니다. 초대 XML Blob은 파일에서 wzInvitation 으로 읽고 PeerGroupJoin에 전달됩니다.

//-----------------------------------------------------------------------------
// Function: JoinGroup
//
// Purpose:  Uses the invitation to join a group with a specific identity.
//           Displays a message if there was an error.
//
// Returns:  HRESULT
//
HRESULT JoinGroup(PCWSTR pwzIdentity, PCWSTR pwzFileName)
{
    HRESULT hr = S_OK;
    WCHAR wzInvitation[MAX_INVITATION] = {0};
    FILE        *file = NULL;
    errno_t     err;

    err = _wfopen_s(&file, pwzFileName, L"rb");
    if (err !=  0)
    {
        hr = E_FAIL;
        DisplayHrError(L"Error opening group invitation file", hr);
        return hr;
    }
    else
    {
        fread(wzInvitation, sizeof(WCHAR), MAX_INVITATION, file);
        if (ferror(file))
        {
            hr = E_FAIL;
            DisplayHrError(L"File read error occurred.", hr);
        }
        fclose(file);

        hr = PeerGroupJoin(pwzIdentity, wzInvitation, NULL, &g_hGroup);
        if (FAILED(hr))
        {
            DisplayHrError(L"Failed to join group.", hr);
        }
    }

    if (SUCCEEDED(hr))
    {
        hr = PrepareToChat( );
    }

    return hr;
}

피어 이벤트 등록

연결하기 전에 애플리케이션과 관련된 모든 피어 이벤트에 등록해야 합니다. 아래 예제에서는 다음 이벤트를 등록합니다.

  • PEER_GROUP_EVENT_RECORD_CHANGED. 레코드는 공개 채팅 메시지를 포함하는 데 사용되므로 새 메시지가 추가될 때마다 애플리케이션에 알림을 받아야 합니다. 이 피어 이벤트가 수신되면 이벤트 데이터는 채팅 메시지와 함께 레코드를 노출합니다. 애플리케이션은 직접 처리하려는 레코드 형식에 대해서만 등록해야 합니다.
  • PEER_GROUP_EVENT_MEMBER_CHANGED. 참가자 목록을 적절하게 업데이트할 수 있도록 멤버가 피어 그룹에 가입하거나 나갈 때 애플리케이션에 알림을 받아야 합니다.
  • PEER_GROUP_EVENT_STATUS_CHANGED. 피어 그룹 상태 변경 내용을 애플리케이션에 전달해야 합니다. 피어 그룹 멤버는 해당 상태 그룹에 연결되고, 피어 그룹 레코드 데이터베이스와 동기화되고, 레코드 업데이트를 적극적으로 수신 대기하는 경우에만 피어 그룹 내에서 사용할 수 있는 것으로 간주됩니다.
  • PEER_GROUP_EVENT_DIRECT_CONNECTION. 두 멤버와 데이터 교환 간의 프라이빗 메시지는 직접 연결을 통해 수행되어야 하므로 애플리케이션이 직접 연결 요청을 처리할 수 있어야 합니다.
  • PEER_GROUP_EVENT_INCOMING_DATA. 직접 연결을 시작한 후 이 피어 이벤트는 프라이빗 메시지가 수신되었음을 애플리케이션에 경고합니다.
//-----------------------------------------------------------------------------
// Function: RegisterForEvents
//
// Purpose:  Registers the EventCallback function so it will be called for only
//           those events that are specified.
//
// Returns:  HRESULT
//
HRESULT RegisterForEvents(void)
{
    HRESULT hr = S_OK;
    PEER_GROUP_EVENT_REGISTRATION regs[] = {
        { PEER_GROUP_EVENT_RECORD_CHANGED, &RECORD_TYPE_CHAT_MESSAGE },
        { PEER_GROUP_EVENT_MEMBER_CHANGED, 0 },
        { PEER_GROUP_EVENT_STATUS_CHANGED, 0 },
        { PEER_GROUP_EVENT_DIRECT_CONNECTION, &DATA_TYPE_WHISPER_MESSAGE },
        { PEER_GROUP_EVENT_INCOMING_DATA, 0 },
    };

    g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (g_hEvent == NULL)
    {
        hr = HRESULT_FROM_WIN32(GetLastError());
    }
    else
    {
        hr = PeerGroupRegisterEvent(g_hGroup, g_hEvent, celems(regs), regs,  &g_hPeerEvent);
    }

    if (SUCCEEDED(hr))
    {
        if (!RegisterWaitForSingleObject(&g_hWait, g_hEvent, EventCallback, NULL, INFINITE, WT_EXECUTEDEFAULT))
        {
            hr = E_UNEXPECTED;
        }
    }

    return hr;
}

피어 그룹에 연결

그룹을 만들거나 조인하고 적절한 이벤트에 등록한 후에는 온라인으로 이동하여 활성 채팅 세션을 시작해야 합니다. 이렇게 하려면 PeerGroupConnect 를 호출하고 PeerGroupCreate 또는 PeerGroupJoin에서 가져온 그룹 핸들을 전달합니다. 이 호출 후 채팅 메시지는 PEER_GROUP_EVENT_RECORD_CHANGED 이벤트로 수신됩니다.

피어 그룹 멤버 목록 가져오기

피어 그룹에 연결된 멤버 목록을 가져오는 것은 간단합니다. PeerGroupEnumMembers 를 호출하여 그룹 멤버 목록을 검색한 다음 모든 멤버가 검색될 때까지 PeerGetNextItem 을 반복적으로 호출합니다. 각 멤버 구조를 처리 한 후 PeerFreeData 를 호출해야 하며 처리가 완료되면 PeerEndEnumeration 을 호출하여 열거형을 닫아야 합니다.

//-----------------------------------------------------------------------------
// Function: UpdateParticipantList
//
// Purpose:  Update the list of partipants.
//
// Returns:  nothing
//
void UpdateParticipantList(void)
{
    HRESULT          hr = S_OK;
    HPEERENUM        hPeerEnum = NULL;
    PEER_MEMBER   ** ppMember = NULL;

    ClearParticipantList( );
    if (NULL == g_hGroup)
    {
        return;
    }

    // Retrieve only the members currently present in the group.
    hr = PeerGroupEnumMembers(g_hGroup, PEER_MEMBER_PRESENT, NULL, &hPeerEnum);
    if (SUCCEEDED(hr))
    {
        while (SUCCEEDED(hr))
        {
            ULONG cItem = 1;
            hr = PeerGetNextItem(hPeerEnum, &cItem, (PVOID **) &ppMember);
            if (SUCCEEDED(hr))
            {
                if (0 == cItem)
                {
                    PeerFreeData(ppMember);
                    break;
                }
            }

            if (SUCCEEDED(hr))
            {
                if (0 != ((*ppMember)->dwFlags & PEER_MEMBER_PRESENT))
                {
                    AddParticipantName((*ppMember)->pwzIdentity, (*ppMember)->pCredentialInfo->pwzFriendlyName);
                }
                PeerFreeData(ppMember);
            }
        }

        PeerEndEnumeration(hPeerEnum);
    }
}

채팅 메시지 보내기

이 예제에서는 채팅 메시지를 PEER_RECORD 구조의 데이터 필드에 배치하여 보냅니다. 레코드는 PeerGroupAddRecord를 호출하여 피어 그룹 레코드에 추가됩니다. 그러면 레코드가 게시되고 피어 그룹에 참여하는 모든 피어에서 PEER_GROUP_EVENT_RECORD_CHANGED 이벤트가 발생합니다.

//-----------------------------------------------------------------------------
// Function: AddChatRecord
//
// Purpose:  This adds a new chat message record to the group.
//
// Returns:  HRESULT
//
HRESULT AddChatRecord(PCWSTR pwzMessage)
{
    HRESULT     hr = S_OK;
    PEER_RECORD record = {0};
    GUID        idRecord;
    ULONGLONG   ulExpire;
    ULONG cch = (ULONG) wcslen(pwzMessage);

    GetSystemTimeAsFileTime((FILETIME *) &ulExpire);

    // Calculate a 2 minute expiration time in 100 nanosecond resolution
    ulExpire += ((ULONGLONG) 60 * 2) * ((ULONGLONG)1000*1000*10);

    // Set up the record
    record.dwSize = sizeof(record);
    record.data.cbData = (cch+1) * sizeof(WCHAR);
    record.data.pbData = (PBYTE) pwzMessage;
    memcpy(&record.ftExpiration, &ulExpire, sizeof(ulExpire));

    PeerGroupUniversalTimeToPeerTime(g_hGroup, &record.ftExpiration, &record.ftExpiration);

    // Set the record type GUID
    record.type = RECORD_TYPE_CHAT_MESSAGE;

    // Add the record to the database
    hr = PeerGroupAddRecord(g_hGroup, &record, &idRecord);
    if (FAILED(hr))
    {
        DisplayHrError(L"Failed to add a chat record to the group.", hr);
    }

    return hr;
}

채팅 메시지 받기

채팅 메시지를 받으려면 아래와 유사한 함수를 호출하는 PEER_GROUP_EVENT_RECORD_CHANGED 이벤트에 대한 콜백 함수를 만듭니다. 레코드는 콜백 함수에서 PeerGroupGetEventData에 대한 이전 호출에서 받은 이벤트 데이터에 대해 PeerGroupGetRecord를 호출하여 가져옵니다. 채팅 메시지는 이 레코드의 데이터 필드에 저장됩니다.

//-----------------------------------------------------------------------------
// Function: ProcessRecordChanged
//
// Purpose:  Processes the PEER_GROUP_EVENT_RECORD_CHANGED event.
//
// Returns:  nothing
//
void ProcessRecordChanged(PEER_EVENT_RECORD_CHANGE_DATA * pData)
{
    switch (pData->changeType)
    {
        case PEER_RECORD_ADDED:
            if (IsEqualGUID(&pData->recordType, &RECORD_TYPE_CHAT_MESSAGE))
            {
                PEER_RECORD * pRecord = {0};
                HRESULT hr = PeerGroupGetRecord(g_hGroup, &pData->recordId, &pRecord);
                if (SUCCEEDED(hr))
                {
                    DisplayChatMessage(pRecord->pwzCreatorId, (PCWSTR) pRecord->data.pbData);
                    PeerFreeData(pRecord);
                }
            }
            break;

        case PEER_RECORD_UPDATED:
        case PEER_RECORD_DELETED:
        case PEER_RECORD_EXPIRED:
            break;

        default:
            break;
    }
}