创建群组聊天应用程序
本主题提供有关使用对等分组 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。 这些可以通过使用其标识名称在被邀请者上调用 PeerIdentityGetXML 来获取。 如果成功, (包含带有 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;
}
邀请(如标识)也在外部发出。 在此示例中,邀请将写入包含 fputw 的 文件中,被邀请者可以在其中获取邀请并在调用 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;
}
}