Lokalne transfery kotwic w programie DirectX

W sytuacjach, w których nie można użyć usługi Azure Spatial Anchors, lokalne transfery kotwic umożliwiają jednemu urządzeniu HoloLens eksportowanie kotwicy do zaimportowania przez drugie urządzenie HoloLens.

Uwaga

Lokalne transfery kotwic zapewniają mniej niezawodną kompletność zakotwiczeń niż usługa Azure Spatial Anchors, a urządzenia z systemami iOS i Android nie są obsługiwane w tym podejściu.

Uwaga

Fragmenty kodu w tym artykule pokazują obecnie użycie języka C++/CX, a nie C++17 zgodnego z językiem C++/WinRT używanego w szablonie projektu holograficznego języka C++. Pojęcia są równoważne projektowi C++/WinRT, chociaż trzeba będzie przetłumaczyć kod.

Transferowanie kotwic przestrzennych

Kotwice przestrzenne można przenosić między urządzeniami Windows Mixed Reality przy użyciu elementu SpatialAnchorTransferManager. Ten interfejs API umożliwia utworzenie kotwicy ze wszystkimi pomocniczymi danymi czujników potrzebnymi do znalezienia tego dokładnego miejsca na świecie, a następnie zaimportowanie tego pakietu na innym urządzeniu. Gdy aplikacja na drugim urządzeniu zaimportowała tę kotwicę, każda aplikacja może renderować hologramy przy użyciu tego udostępnionego systemu współrzędnych kotwicy przestrzennej, który pojawi się w tym samym miejscu w świecie rzeczywistym.

Należy pamiętać, że kotwice przestrzenne nie mogą być przenoszone między różnymi typami urządzeń, na przykład kotwica przestrzenna urządzenia HoloLens może nie być lokalizowalny przy użyciu immersywnego zestawu nagłownego. Przeniesione kotwice nie są również zgodne z urządzeniami z systemem iOS lub Android.

Konfigurowanie aplikacji do korzystania z funkcji spatialPerception

Aby można było korzystać z funkcji SpatialPerception, aplikacja musi mieć uprawnienie do korzystania z obiektu SpatialAnchorTransferManager. Jest to konieczne, ponieważ transfer kotwicy przestrzennej obejmuje udostępnianie obrazów czujników zebranych w czasie w pobliżu tej kotwicy, która może zawierać poufne informacje.

Zadeklaruj tę możliwość w pliku package.appxmanifest dla aplikacji. Oto przykład:

<Capabilities>
  <uap2:Capability Name="spatialPerception" />
</Capabilities>

Możliwość pochodzi z przestrzeni nazw uap2 . Aby uzyskać dostęp do tej przestrzeni nazw w manifeście, dołącz go jako atrybut xlmns w elemecie <Package> . Oto przykład:

<Package
    xmlns="https://schemas.microsoft.com/appx/manifest/foundation/windows10"
    xmlns:mp="https://schemas.microsoft.com/appx/2014/phone/manifest"
    xmlns:uap="https://schemas.microsoft.com/appx/manifest/uap/windows10"
    xmlns:uap2="https://schemas.microsoft.com/appx/manifest/uap/windows10/2"
    IgnorableNamespaces="uap mp"
    >

UWAGA: Aplikacja będzie musiała zażądać możliwości w czasie wykonywania, zanim będzie mogła uzyskać dostęp do interfejsów API eksportu/importowania usługi SpatialAnchor. Zobacz RequestAccessAsync w poniższych przykładach.

Serializowanie danych kotwicy przez wyeksportowanie ich za pomocą elementu SpatialAnchorTransferManager

Funkcja pomocnika jest uwzględniona w przykładowym kodzie do eksportowania (serializowanie) danych SpatialAnchor . Ten interfejs API eksportu serializuje wszystkie kotwice w kolekcji par klucz-wartość kojarzące ciągi z kotwicami.

// ExportAnchorDataAsync: Exports a byte buffer containing all of the anchors in the given collection.
//
// This function will place data in a buffer using a std::vector<byte>. The ata buffer contains one or more
// Anchors if one or more Anchors were successfully imported; otherwise, it is ot modified.
//
task<bool> SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
    vector<byte>* anchorByteDataOut,
    IMap<String^, SpatialAnchor^>^ anchorsToExport
    )
{

Najpierw musimy skonfigurować strumień danych. Pozwoli nam to na 1.) Użyj polecenia TryExportAnchorsAsync, aby umieścić dane w buforze należącym do aplikacji i 2. odczytywać dane ze strumienia wyeksportowanego buforu bajtów — czyli strumienia danych WinRT — do naszego własnego bufora pamięci, który jest bajtem> std::vector<.

// Create a random access stream to process the anchor byte data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor byte stream.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);

Musimy poprosić o uprawnienie dostępu do danych przestrzennych, w tym kotwic eksportowanych przez system.

// Request access to spatial data.
auto accessRequestedTask = create_taskSpatialAnchorTransferManager::RequestAccessAsync()).then([anchorsToExport, utputStream](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.
        // Export the indicated set of anchors.
        return create_task(SpatialAnchorTransferManager::TryExportAnchorsAsync(
            anchorsToExport,
            outputStream
            ));
    }
    else
    {
        // Access is denied.
        return task_from_result<bool>(false);
    }
});

Jeśli uzyskamy uprawnienia i kotwice zostaną wyeksportowane, możemy odczytać strumień danych. W tym miejscu pokazano również, jak utworzyć element DataReader i InputStream, który będzie używany do odczytywania danych.

// Get the input stream for the anchor byte stream.
IInputStream^ inputStream = stream->GetInputStreamAt(0);
// Create a DataReader, to get bytes from the anchor byte stream.
DataReader^ reader = ref new DataReader(inputStream);
return accessRequestedTask.then([anchorByteDataOut, stream, reader](bool nchorsExported)
{
    if (anchorsExported)
    {
        // Get the size of the exported anchor byte stream.
        size_t bufferSize = static_cast<size_t>(stream->Size);
        // Resize the output buffer to accept the data from the stream.
        anchorByteDataOut->reserve(bufferSize);
        anchorByteDataOut->resize(bufferSize);
        // Read the exported anchor store into the stream.
        return create_task(reader->LoadAsync(bufferSize));
    }
    else
    {
        return task_from_result<size_t>(0);
    }

Po odczytaniu bajtów ze strumienia możemy je zapisać we własnym buforze danych w następujący sposób.

}).then([anchorByteDataOut, reader](size_t bytesRead)
{
    if (bytesRead > 0)
    {
        // Read the bytes from the stream, into our data output buffer.
        reader->ReadBytes(Platform::ArrayReference<byte>(&(*anchorByteDataOut)[0], bytesRead));
        return true;
    }
    else
    {
        return false;
    }
});
};

Deserializacji danych kotwicy przez zaimportowanie ich do systemu przy użyciu elementu SpatialAnchorTransferManager

Funkcja pomocnika jest zawarta w przykładzie kodu w celu załadowania wcześniej wyeksportowanych danych. Ta funkcja deserializacji udostępnia kolekcję par klucz-wartość, podobnie jak zapewnia funkcja SpatialAnchorStore , z tą różnicą, że te dane pochodzą z innego źródła, takiego jak gniazdo sieciowe. Przed zapisaniem ich w trybie offline, przy użyciu pamięci w aplikacji lub (jeśli ma to zastosowanie) aplikacji SpatialAnchorStore, możesz przetworzyć i przyczyny dotyczące tych danych.

// ImportAnchorDataAsync: Imports anchors from a byte buffer that was previously exported.
//
// This function will import all anchors from a data buffer into an in-memory ollection of key, value
// pairs that maps String objects to SpatialAnchor objects. The Spatial nchorStore is not affected by
// this function unless you provide it as the target collection for import.
//
task<bool> SpatialAnchorImportExportHelper::ImportAnchorDataAsync(
    std::vector<byte>& anchorByteDataIn,
    IMap<String^, SpatialAnchor^>^ anchorMapOut
    )
{

Najpierw musimy utworzyć obiekty strumienia, aby uzyskać dostęp do danych kotwicy. Będziemy zapisywać dane z buforu systemu do bufora systemowego, dlatego utworzymy element DataWriter, który zapisuje dane w strumieniu danych w pamięci, aby osiągnąć nasz cel w celu uzyskania kotwic z buforu bajtowego do systemu jako elementu SpatialAnchors.

// Create a random access stream for the anchor data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor data.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);
// Create a writer, to put the bytes in the stream.
DataWriter^ writer = ref new DataWriter(outputStream);

Ponownie musimy upewnić się, że aplikacja ma uprawnienia do eksportowania danych kotwicy przestrzennej, które mogą zawierać prywatne informacje o środowisku użytkownika.

// Request access to transfer spatial anchors.
return create_task(SpatialAnchorTransferManager::RequestAccessAsync()).then(
    [&anchorByteDataIn, writer](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.

Jeśli dostęp jest dozwolony, możemy zapisywać bajty z buforu do strumienia danych systemowych.

// Write the bytes to the stream.
        byte* anchorDataFirst = &anchorByteDataIn[0];
        size_t anchorDataSize = anchorByteDataIn.size();
        writer->WriteBytes(Platform::ArrayReference<byte>(anchorDataFirst, anchorDataSize));
        // Store the stream.
        return create_task(writer->StoreAsync());
    }
    else
    {
        // Access is denied.
        return task_from_result<size_t>(0);
    }

Jeśli udało nam się zapisać bajty w strumieniu danych, możemy spróbować zaimportować te dane przy użyciu klasy SpatialAnchorTransferManager.

}).then([writer, stream](unsigned int bytesWritten)
{
    if (bytesWritten > 0)
    {
        // Try to import anchors from the byte stream.
        return create_task(writer->FlushAsync())
            .then([stream](bool dataWasFlushed)
        {
            if (dataWasFlushed)
            {
                // Get the input stream for the anchor data.
                IInputStream^ inputStream = stream->GetInputStreamAt(0);
                return create_task(SpatialAnchorTransferManager::TryImportAnchorsAsync(inputStream));
            }
            else
            {
                return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
            }
        });
    }
    else
    {
        return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
    }

Jeśli dane można zaimportować, uzyskamy widok mapy par klucz-wartość kojarzący ciągi z kotwicami. Możemy załadować je do własnej kolekcji danych w pamięci i użyć tej kolekcji, aby wyszukać kotwice, których chcemy użyć.

}).then([anchorMapOut](task<Windows::Foundation::Collections::IMapView<String^, SpatialAnchor^>^>  previousTask)
{
    try
    {
        auto importedAnchorsMap = previousTask.get();
        // If the operation was successful, we get a set of imported anchors.
        if (importedAnchorsMap != nullptr)
        {
            for each (auto& pair in importedAnchorsMap)
            {
                // Note that you could look for specific anchors here, if you know their key values.
                auto const& id = pair->Key;
                auto const& anchor = pair->Value;
                // Append "Remote" to the end of the anchor name for disambiguation.
                std::wstring idRemote(id->Data());
                idRemote += L"Remote";
                String^ idRemoteConst = ref new String (idRemote.c_str());
                // Store the anchor in the current in-memory anchor map.
                anchorMapOut->Insert(idRemoteConst, anchor);
            }
            return true;
        }
    }
    catch (Exception^ exception)
    {
        OutputDebugString(L"Error: Unable to import the anchor data buffer bytes into the in-memory anchor collection.\n");
    }
    return false;
});
}

UWAGA: Tylko dlatego, że można zaimportować kotwicę, nie musi oznaczać, że można go używać od razu. Kotwica może znajdować się w innym pomieszczeniu lub w innym miejscu fizycznym w całości; kotwica nie będzie można zlokalizować, dopóki urządzenie, które odebrało, ma wystarczającą ilość informacji wizualnych o środowisku, w których utworzono kotwicę, aby przywrócić pozycję kotwicy względem znanego bieżącego środowiska. Implementacja klienta powinna spróbować zlokalizować kotwicę względem lokalnego systemu współrzędnych lub ramki referencyjnej przed kontynuowaniem próby użycia jej do zawartości na żywo. Na przykład spróbuj zlokalizować kotwicę względem bieżącego układu współrzędnych okresowo, dopóki kotwica nie zacznie znajdować się.

Kwestie szczególne

Interfejs API TryExportAnchorsAsync umożliwia eksportowanie wielu obiektów SpatialAnchors do tego samego nieprzezroczystego binarnego obiektu blob. Istnieje jednak subtelna różnica między danymi, które będą zawierać obiekt blob, w zależności od tego, czy w jednym wywołaniu jest eksportowany pojedynczy obiekt SpatialAnchor czy wiele obiektów SpatialAnchor.

Eksportowanie pojedynczego elementu SpatialAnchor

Obiekt blob zawiera reprezentację środowiska w pobliżu obiektu SpatialAnchor, dzięki czemu środowisko można rozpoznać na urządzeniu, które importuje obiekt SpatialAnchor. Po zakończeniu importowania nowy element SpatialAnchor będzie dostępny dla urządzenia. Zakładając, że użytkownik znajduje się niedawno w pobliżu kotwicy, będzie można renderować lokalizatory i hologramy dołączone do elementu SpatialAnchor. Te hologramy będą wyświetlane w tej samej lokalizacji fizycznej, którą wykonali na oryginalnym urządzeniu, które wyeksportowało spatialAnchor.

Eksportowanie pojedynczego elementu SpatialAnchor

Eksportowanie wielu obiektów SpatialAnchor

Podobnie jak w przypadku eksportu pojedynczego obiektu SpatialAnchor obiekt blob zawiera reprezentację środowiska w pobliżu wszystkich określonych obiektów SpatialAnchors. Ponadto obiekt blob zawiera informacje o połączeniach między dołączonymi elementami SpatialAnchors, jeśli znajdują się w tej samej przestrzeni fizycznej. Oznacza to, że jeśli zostaną zaimportowane dwie pobliskie elementy SpatialAnchor, hologram dołączony do drugiego obiektu SpatialAnchor będzie lokalizowany, nawet jeśli urządzenie rozpoznaje tylko środowisko wokół pierwszego obiektu SpatialAnchor, ponieważ wystarczająca ilość danych do obliczenia przekształcenia między dwoma elementami SpatialAnchor została uwzględniona w obiekcie blob. Jeśli dwa elementy SpatialAnchors zostały wyeksportowane pojedynczo (dwa oddzielne wywołania funkcji TryExportSpatialAnchors), wówczas w obiekcie blob może być za mało danych zawartych w obiekcie blob dla hologramów dołączonych do drugiego obiektu SpatialAnchor, które mają być lokalizowane, gdy znajduje się pierwszy.

Wiele kotwic wyeksportowanych przy użyciu pojedynczego wywołania TryExportAnchorsAsyncwielu kotwic wyeksportowanych przy użyciu oddzielnego wywołania TryExportAnchorsAsync dla każdej kotwicy

Przykład: wysyłanie danych kotwicy przy użyciu elementu Windows::Networking::StreamSocket

W tym miejscu przedstawiono przykład użycia wyeksportowanych danych kotwicy przez wysłanie ich przez sieć TCP. To jest z HolographicSpatialAnchorTransferSample.

Klasa WinRT StreamSocket używa biblioteki zadań PPL. W przypadku błędów sieci błąd jest zwracany do następnego zadania w łańcuchu przy użyciu wyjątku, który jest zgłaszany ponownie. Wyjątek zawiera wartość HRESULT wskazującą stan błędu.

Używanie elementu Windows::Networking::StreamSocketListener z protokołem TCP do wysyłania wyeksportowanych danych kotwicy

Utwórz wystąpienie serwera, które nasłuchuje połączenia.

void SampleAnchorTcpServer::ListenForConnection()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocketListener^ streamSocketListener = m_socketServer;
    if (streamSocketListener == nullptr)
    {
        OutputDebugString(L"Server listening for client.\n");
        // Create the web socket connection.
        streamSocketListener = ref new StreamSocketListener();
        streamSocketListener->Control->KeepAlive = true;
        streamSocketListener->BindEndpointAsync(
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        streamSocketListener->ConnectionReceived +=
            ref new Windows::Foundation::TypedEventHandler<StreamSocketListener^, StreamSocketListenerConnectionReceivedEventArgs^>(
                std::bind(&SampleAnchorTcpServer::OnConnectionReceived, this, _1, _2)
                );
        m_socketServer = streamSocketListener;
    }
    else
    {
        OutputDebugString(L"Error: Stream socket listener not created.\n");
    }
}

Po odebraniu połączenia użyj połączenia gniazda klienta, aby wysłać dane kotwicy.

void SampleAnchorTcpServer::OnConnectionReceived(StreamSocketListener^ listener, StreamSocketListenerConnectionReceivedEventArgs^ args)
{
    m_socketForClient = args->Socket;
    if (m_socketForClient != nullptr)
    {
        // In this example, when the client first connects, we catch it up to the current state of our anchor set.
        OutputToClientSocket(m_spatialAnchorHelper->GetAnchorMap());
    }
}

Teraz możemy rozpocząć wysyłanie strumienia danych zawierającego wyeksportowane dane zakotwiczenia.

void SampleAnchorTcpServer::OutputToClientSocket(IMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    m_anchorTcpSocketStreamWriter = ref new DataWriter(m_socketForClient->OutputStream);
    OutputDebugString(L"Sending stream to client.\n");
    SendAnchorDataStream(anchorsToSend).then([this](task<bool> previousTask)
    {
        try
        {
            bool success = previousTask.get();
            if (success)
            {
                OutputDebugString(L"Anchor data sent!\n");
            }
            else
            {
                OutputDebugString(L"Error: Anchor data not sent.\n");
            }
        }
        catch (Exception^ exception)
        {
            HandleException(exception);
            OutputDebugString(L"Error: Anchor data was not sent.\n");
        }
    });
}

Zanim wyślemy sam strumień, musimy najpierw wysłać pakiet nagłówka. Ten pakiet nagłówka musi mieć stałą długość i musi również wskazywać długość zmiennej tablicy bajtów, która jest strumieniem danych zakotwiczenia; w tym przykładzie nie mamy żadnych innych danych nagłówka do wysłania, więc nagłówek ma 4 bajty długości i zawiera 32-bitową liczbę całkowitą bez znaku.

Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataLengthMessage(size_t dataStreamLength)
{
    unsigned int arrayLength = dataStreamLength;
    byte* data = reinterpret_cast<byte*>(&arrayLength);
    m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    return create_task(m_anchorTcpSocketStreamWriter->StoreAsync()).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            OutputDebugString(L"Anchor data length stored in stream; Flushing stream.\n");
            return create_task(m_anchorTcpSocketStreamWriter->FlushAsync());
        }
        else
        {
            OutputDebugString(L"Error: Anchor data length not stored in stream.\n");
            return task_from_result<bool>(false);
        }
    });
}
Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataStreamIMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    return SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
        &m_exportedAnchorStoreBytes,
        anchorsToSend
        ).then([this](bool anchorDataExported)
    {
        if (anchorDataExported)
        {
            const size_t arrayLength = m_exportedAnchorStoreBytes.size();
            if (arrayLength > 0)
            {
                OutputDebugString(L"Anchor data was exported; sending data stream length message.\n");
                return SendAnchorDataLengthMessage(arrayLength);
            }
        }
        OutputDebugString(L"Error: Anchor data was not exported.\n");
        // No data to send.
        return task_from_result<bool>(false);

Po wysłaniu strumienia w bajtach do klienta możemy kontynuować zapisywanie strumienia danych do strumienia gniazda. Spowoduje to wysłanie bajtów magazynu kotwicy do klienta.

}).then([this](bool dataLengthSent)
    {
        if (dataLengthSent)
        {
            OutputDebugString(L"Data stream length message sent; writing exported anchor store bytes to stream.\n");
            m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0], m_exportedAnchorStoreBytes.size()));
            return create_task(m_anchorTcpSocketStreamWriter->StoreAsync());
        }
        else
        {
            OutputDebugString(L"Error: Data stream length message not sent.\n");
            return task_from_result<size_t>(0);
        }
    }).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            PrintWstringToDebugConsole(
                std::to_wstring(bytesStored) +
                L" bytes of anchor data written and stored to stream; flushing stream.\n"
                );
        }
        else
        {
            OutputDebugString(L"Error: No anchor data bytes were written to the stream.\n");
        }
        return task_from_result<bool>(false);
    });
}

Jak wspomniano wcześniej w tym temacie, musimy być przygotowani do obsługi wyjątków zawierających komunikaty o stanie błędu sieci. W przypadku błędów, które nie są oczekiwane, możemy zapisać informacje o wyjątku w konsoli debugowania w następujący sposób. Dzięki temu możemy dowiedzieć się, co się stało, jeśli nasz przykład kodu nie może ukończyć połączenia lub jeśli nie może zakończyć wysyłania danych kotwicy.

void SampleAnchorTcpServer::HandleException(Exception^ exception)
{
    PrintWstringToDebugConsole(
        std::wstring(L"Connection error: ") +
        exception->ToString()->Data() +
        L"\n"
        );
}

Odbieranie wyeksportowanych danych kotwicy przy użyciu protokołu Windows::Networking::StreamSocket z protokołem TCP

Najpierw musimy nawiązać połączenie z serwerem. W tym przykładzie kodu pokazano, jak utworzyć i skonfigurować protokół StreamSocket oraz utworzyć element DataReader, którego można użyć do uzyskiwania danych sieciowych przy użyciu połączenia gniazda.

UWAGA: Jeśli uruchomisz ten przykładowy kod, przed uruchomieniem klienta upewnij się, że serwer został skonfigurowany i uruchomiony.

task<bool> SampleAnchorTcpClient::ConnectToServer()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocket^ streamSocket = m_socketClient;
    // Have we connected yet?
    if (m_socketClient == nullptr)
    {
        OutputDebugString(L"Client is attempting to connect to server.\n");
        EndpointPair^ endpointPair = ref new EndpointPair(
            SampleAnchorTcpCommon::m_clientHost,
            SampleAnchorTcpCommon::m_tcpPort,
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        // Create the web socket connection.
        m_socketClient = ref new StreamSocket();
        // The client connects to the server.
        return create_task(m_socketClient->ConnectAsync(endpointPair, SocketProtectionLevel::PlainSocket)).then([this](task<void> previousTask)
        {
            try
            {
                // Try getting all exceptions from the continuation chain above this point.
                previousTask.get();
                m_anchorTcpSocketStreamReader = ref new DataReader(m_socketClient->InputStream);
                OutputDebugString(L"Client connected!\n");
                m_anchorTcpSocketStreamReader->InputStreamOptions = InputStreamOptions::ReadAhead;
                WaitForAnchorDataStream();
                return true;
            }
            catch (Exception^ exception)
            {
                if (exception->HResult == 0x80072741)
                {
                    // This code sample includes a very simple implementation of client/server
                    // endpoint detection: if the current instance tries to connect to itself,
                    // it is determined to be the server.
                    OutputDebugString(L"Starting up the server instance.\n");
                    // When we return false, we'll start up the server instead.
                    return false;
                }
                else if ((exception->HResult == 0x8007274c) || // connection timed out
                    (exception->HResult == 0x80072740)) // connection maxed at server end
                {
                    // If the connection timed out, try again.
                    ConnectToServer();
                }
                else if (exception->HResult == 0x80072741)
                {
                    // No connection is possible.
                }
                HandleException(exception);
                return true;
            }
        });
    }
    else
    {
        OutputDebugString(L"A StreamSocket connection to a server already exists.\n");
        return task_from_result<bool>(true);
    }
}

Po nawiązaniu połączenia możemy poczekać na wysłanie danych przez serwer. W tym celu wywołujemy funkcję LoadAsync w czytniku danych strumienia.

Pierwszy zestaw odbieranych bajtów powinien być zawsze pakietem nagłówka, który wskazuje długość bajtu strumienia danych kotwicy zgodnie z opisem w poprzedniej sekcji.

void SampleAnchorTcpClient::WaitForAnchorDataStream()
{
    if (m_anchorTcpSocketStreamReader == nullptr)
    {
        // We have not connected yet.
        return;
    }
    OutputDebugString(L"Waiting for server message.\n");
    // Wait for the first message, which specifies the byte length of the string data.
    create_task(m_anchorTcpSocketStreamReader->LoadAsync(SampleAnchorTcpCommon::c_streamHeaderByteArrayLength)).then([this](unsigned int numberOfBytes)
    {
        if (numberOfBytes > 0)
        {
            OutputDebugString(L"Server message incoming.\n");
            return ReceiveAnchorDataLengthMessage();
        }
        else
        {
            OutputDebugString(L"0-byte async task received, awaiting server message again.\n");
            WaitForAnchorDataStream();
            return task_from_result<size_t>(0);
        }

...

task<size_t> SampleAnchorTcpClient::ReceiveAnchorDataLengthMessage()
{
    byte data[4];
    m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    unsigned int lengthMessageSize = *reinterpret_cast<unsigned int*>(data);
    if (lengthMessageSize > 0)
    {
        OutputDebugString(L"One or more anchors to be received.\n");
        return task_from_result<size_t>(lengthMessageSize);
    }
    else
    {
        OutputDebugString(L"No anchors to be received.\n");
        ConnectToServer();
    }
    return task_from_result<size_t>(0);
}

Po otrzymaniu pakietu nagłówka wiemy, ile bajtów danych kotwicy powinniśmy oczekiwać. Możemy kontynuować odczytywanie tych bajtów ze strumienia.

}).then([this](size_t dataStreamLength)
    {
        if (dataStreamLength > 0)
        {
            std::wstring debugMessage = std::to_wstring(dataStreamLength);
            debugMessage += L" bytes of anchor data incoming.\n";
            OutputDebugString(debugMessage.c_str());
            // Prepare to receive the data stream in one or more pieces.
            m_anchorStreamLength = dataStreamLength;
            m_exportedAnchorStoreBytes.clear();
            m_exportedAnchorStoreBytes.resize(m_anchorStreamLength);
            OutputDebugString(L"Loading byte stream.\n");
            return ReceiveAnchorDataStream();
        }
        else
        {
            OutputDebugString(L"Error: Anchor data size not received.\n");
            ConnectToServer();
            return task_from_result<bool>(false);
        }
    });
}

Oto nasz kod do odbierania strumienia danych zakotwiczenia. Ponownie najpierw załadujemy bajty ze strumienia; wykonanie tej operacji może zająć trochę czasu, ponieważ oczekiwanie na odebranie tej ilości bajtów z sieci przez funkcję StreamSocket.

Po zakończeniu operacji ładowania możemy odczytać tę liczbę bajtów. Jeśli otrzymaliśmy liczbę bajtów, których oczekujemy dla strumienia danych zakotwiczenia, możemy przejść do przodu i zaimportować dane zakotwiczone; jeśli nie, musi istnieć jakiś błąd. Na przykład może się to zdarzyć, gdy wystąpienie serwera zakończy działanie, zanim zakończy wysyłanie strumienia danych, lub sieć ulegnie awarii, zanim cały strumień danych będzie mógł zostać odebrany przez klienta.

task<bool> SampleAnchorTcpClient::ReceiveAnchorDataStream()
{
    if (m_anchorStreamLength > 0)
    {
        // First, we load the bytes from the network socket.
        return create_task(m_anchorTcpSocketStreamReader->LoadAsync(m_anchorStreamLength)).then([this](size_t bytesLoadedByStreamReader)
        {
            if (bytesLoadedByStreamReader > 0)
            {
                // Once the bytes are loaded, we can read them from the stream.
                m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0],
                    bytesLoadedByStreamReader));
                // Check status.
                if (bytesLoadedByStreamReader == m_anchorStreamLength)
                {
                    // The whole stream has arrived. We can process the data.
                    // Informational message of progress complete.
                    std::wstring infoMessage = std::to_wstring(bytesLoadedByStreamReader);
                    infoMessage += L" bytes read out of ";
                    infoMessage += std::to_wstring(m_anchorStreamLength);
                    infoMessage += L" total bytes; importing the data.\n";
                    OutputDebugStringW(infoMessage.c_str());
                    // Kick off a thread to wait for a new message indicating another incoming anchor data stream.
                    WaitForAnchorDataStream();
                    // Process the data for the stream we just received.
                    return SpatialAnchorImportExportHelper::ImportAnchorDataAsync(m_exportedAnchorStoreBytes, m_spatialAnchorHelper->GetAnchorMap());
                }
                else
                {
                    OutputDebugString(L"Error: Fewer than expected anchor data bytes were received.\n");
                }
            }
            else
            {
                OutputDebugString(L"Error: No anchor bytes were received.\n");
            }
            return task_from_result<bool>(false);
        });
    }
    else
    {
        OutputDebugString(L"Warning: A zero-length data buffer was sent.\n");
        return task_from_result<bool>(false);
    }
}

Ponownie musimy być przygotowani do obsługi nieznanych błędów sieci.

void SampleAnchorTcpClient::HandleException(Exception^ exception)
{
    std::wstring error = L"Connection error: ";
    error += exception->ToString()->Data();
    error += L"\n";
    OutputDebugString(error.c_str());
}

Gotowe. Teraz należy mieć wystarczającą ilość informacji, aby spróbować zlokalizować kotwice odebrane za pośrednictwem sieci. Ponownie należy pamiętać, że klient musi mieć wystarczającą ilość danych śledzenia wizualnego dla miejsca, aby pomyślnie zlokalizować kotwicę; Jeśli nie działa od razu, spróbuj chodzić przez chwilę. Jeśli nadal nie działa, serwer wysyła więcej kotwic i używa komunikacji sieciowej, aby uzgodnić, który działa dla klienta. Możesz to wypróbować, pobierając element HolographicSpatialAnchorTransferSample, konfigurując adresy IP klienta i serwera oraz wdrażając je na urządzeniach klienckich i serwerowych HoloLens.

Zobacz też