Kanál pro segmentaci dat

Ukázka ChunkingChannelu ukazuje, jak lze vlastní protokol nebo vrstvený kanál použít k chunkování a dechunkování libovolně velkých zpráv.

Při odesílání velkých zpráv pomocí technologie Windows Communication Foundation (WCF) je často žádoucí omezit množství paměti používané k ukládání těchto zpráv do vyrovnávací paměti. Jedním z možných řešení je streamování textu zprávy (za předpokladu, že je většina dat v těle). Některé protokoly však vyžadují ukládání do vyrovnávací paměti celé zprávy. Spolehlivé zasílání zpráv a zabezpečení jsou dva takové příklady. Dalším možným řešením je rozdělit velkou zprávu na menší zprávy označované jako bloky dat, odeslat tyto bloky po jednom bloku a rozředět velkou zprávu na přijímající straně. Samotná aplikace může provést toto vytváření bloků dat a odstranění bloků dat nebo k tomu může použít vlastní kanál.

Dílčí zpracování by se mělo vždy použít až poté, co byla celá zpráva, která se má odeslat, vytvořena. Blokovací kanál by měl být vždy vrstvený pod kanálem zabezpečení a spolehlivým kanálem relace.

Poznámka:

Postup nastavení a pokyny k sestavení pro tuto ukázku najdete na konci tohoto tématu.

Předpoklady a omezení kanálu pro dělení na části

Struktura zpráv

Kanál pro segmentaci předpokládá následující strukturu zpráv pro segmentaci zpráv.

<soap:Envelope>
  <!-- headers -->
  <soap:Body>
    <operationElement>
      <paramElement>data to be chunked</paramElement>
    </operationElement>
  </soap:Body>
</soap:Envelope>

Při použití modelu ServiceModel operace kontraktu, které mají 1 vstupní parametr, vyhovují tomuto tvaru zprávy pro jejich vstupní zprávu. Podobně operace kontraktů, které mají 1 výstupní parametr nebo návratovou hodnotu, odpovídají tomuto tvaru zprávy pro jejich výstupní zprávu. Tady jsou příklady těchto operací:

[ServiceContract]
interface ITestService
{
    [OperationContract]
    Stream EchoStream(Stream stream);

    [OperationContract]
    Stream DownloadStream();

    [OperationContract(IsOneWay = true)]
    void UploadStream(Stream stream);
}

Přednášky

Kanál pro segmentaci vyžaduje, aby byly zprávy doručeny přesně jednou, v pořadí podle seřazených doručení zpráv (segmentů). To znamená, že základní zásobník kanálu musí být relační. Relace mohou být poskytovány přenosem (například přenosem TCP) nebo kanálem protokolu s podporou relací (například kanálem ReliableSession).

Asynchronní odesílání a příjem

Asynchronní metody odesílání a příjmu nejsou v této verzi ukázky kanálu bloků dat implementovány.

Protokol dělení na části

Kanál bloků dat definuje protokol, který označuje začátek a konec řady bloků dat a také pořadové číslo každého bloku dat. Následující tři ukázkové zprávy ukazují počáteční, blokové a koncové zprávy s komentáři, které popisují klíčové aspekty každého.

Úvodní zpráva

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
<!--Original message action is replaced with a chunking-specific action. -->
    <a:Action s:mustUnderstand="1">http://samples.microsoft.com/chunkingAction</a:Action>
<!--
Original message is assigned a unique id that is transmitted
in a MessageId header. Note that this is different from the WS-Addressing MessageId header.
-->
    <MessageId s:mustUnderstand="1" xmlns="http://samples.microsoft.com/chunking">
53f183ee-04aa-44a0-b8d3-e45224563109
</MessageId>
<!--
ChunkingStart header signals the start of a chunked message.
-->
    <ChunkingStart s:mustUnderstand="1" i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://samples.microsoft.com/chunking" />
<!--
Original message action is transmitted in OriginalAction.
This is required to re-create the original message on the other side.
-->
    <OriginalAction xmlns="http://samples.microsoft.com/chunking">
http://tempuri.org/ITestService/EchoStream
    </OriginalAction>
   <!--
    All original message headers are included here.
   -->
  </s:Header>
  <s:Body>
<!--
Chunking assumes this structure of Body content:
<element>
  <childelement>large data to be chunked<childelement>
</element>
The start message contains just <element> and <childelement> without
the data to be chunked.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Část zprávy

<s:Envelope
  xmlns:a="http://www.w3.org/2005/08/addressing"
  xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
   <!--
    All chunking protocol messages have this action.
   -->
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
The sequence number of the chunk.
This number restarts at 1 with each new sequence of chunks.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      1096
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The chunked data is wrapped in a chunk element.
The encoding of this data (and the entire message)
depends on the encoder used. The chunking channel does not mandate an encoding.
-->
    <chunk xmlns="http://samples.microsoft.com/chunking">
kfSr2QcBlkHTvQ==
    </chunk>
  </s:Body>
</s:Envelope>

Ukončit zprávu

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
ChunkingEnd header signals the end of a chunk sequence.
-->
    <ChunkingEnd s:mustUnderstand="1" i:nil="true"
                 xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns="http://samples.microsoft.com/chunking" />
<!--
ChunkingEnd messages have a sequence number.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      79
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The ChunkingEnd message has the same <element><childelement> structure
as the ChunkingStart message.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Architektura bloků kanálů

Kanál pro vytváření bloků dat je takový IDuplexSessionChannel , že na vysoké úrovni se řídí typickou architekturou kanálu. Existuje ChunkingBindingElement, který může vytvořit ChunkingChannelFactory a ChunkingChannelListener. Vytvoří ChunkingChannelFactory instance ChunkingChannel, když je o to požádán. Systém ChunkingChannelListener vytvoří instance ChunkingChannel při přijetí nového vnitřního kanálu. Samotný ChunkingChannel je zodpovědný za odesílání a přijímání zpráv.

Na další úrovni dolů ChunkingChannel spoléhá na několik komponent pro implementaci protokolu bloků dat. Na straně odeslání kanál používá vlastní prvek XmlDictionaryWriter, nazývaný ChunkingWriter, který provede skutečné rozdělení na bloky. ChunkingWriter používá vnitřní kanál přímo k odesílání bloků dat. Použití vlastního XmlDictionaryWriter nám umožňuje odesílat části, jak je zapisováno velké tělo původní zprávy. To znamená, že neukládáme do vyrovnávací paměti celou původní zprávu.

Diagram znázorňující architekturu odesílání bloků kanálu.

Na přijímací straně ChunkingChannel stáhne zprávy z vnitřního kanálu a předávání je speciálnímu XmlDictionaryReader nazvanému ChunkingReader, který rekonstituuje původní zprávu z příchozích chunků. ChunkingChannel zabalí toto ChunkingReader do vlastní Message implementace nazývané ChunkingMessage a odešle tuto zprávu do vrstvy výše. Tato kombinace ChunkingReader a ChunkingMessage umožňuje de-chunkovat původní text zprávy, protože je přečtena vrstvou výše, a nemusí ukládat do vyrovnávací paměti celý původní text zprávy. ChunkingReader obsahuje frontu, ve které uloží příchozí bloky do vyrovnávací paměti až do maximálního konfigurovatelného počtu bloků dat v vyrovnávací paměti. Když dosáhnete tohoto maximálního limitu, čtenář čeká na vyprázdnění zpráv z fronty vrstvou výše (tj. čtením z původního textu zprávy) nebo až do dosažení maximálního časového limitu příjmu.

Diagram znázorňující architekturu příjmu kanálu bloků dat

Vytváření bloků programovacího modelu

Vývojáři služeb můžou určit, které zprávy mají být rozdíleny do bloků, použitím atributu ChunkingBehavior na operace v rámci kontraktu. Atribut zveřejňuje AppliesTo vlastnost, která umožňuje vývojáři určit, zda se bloky dat vztahují na vstupní zprávu, výstupní zprávu nebo obojí. Následující příklad ukazuje použití atributu ChunkingBehavior :

[ServiceContract]
interface ITestService
{
    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.Both)]
    Stream EchoStream(Stream stream);

    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.OutMessage)]
    Stream DownloadStream();

    [OperationContract(IsOneWay=true)]
    [ChunkingBehavior(ChunkingAppliesTo.InMessage)]
    void UploadStream(Stream stream);

}

Z tohoto programovacího modelu ChunkingBindingElement zkompiluje seznam identifikátorů URI akcí, které identifikují zprávy, jež mají být rozděleny na části. Akce každé odchozí zprávy se porovná s tímto seznamem a určí, jestli má být zpráva rozdělena nebo odeslána přímo.

Implementace operace odeslání

Obecně operace Odeslání nejprve zkontroluje, jestli musí být odchozí zpráva rozčleněna, a pokud ne, odešle zprávu přímo pomocí vnitřního kanálu.

Pokud zpráva musí být rozdělena na části, Send vytvoří novou ChunkingWriter a zavolá WriteBodyContents na odchozí zprávu, předávající jí tuto ChunkingWriter. Poté ChunkingWriter provede rozdělení zprávy na bloky (včetně kopírování původních hlaviček zpráv do počátečního bloku zprávy) a odesílá bloky pomocí vnitřního kanálu.

Několik podrobností stojí za zmínku:

  • Odešlete první volání ThrowIfDisposedOrNotOpened, abyste měli jistotu, že CommunicationState je otevřený.

  • Odesílání se synchronizuje, aby se pro každou relaci posílala vždy jenom jedna zpráva. Je tu ManualResetEvent, který se resetuje při odesílání blokované zprávy a má název sendingDone. Po odeslání zprávy koncového bloku je tato událost nastavena. Metoda Send čeká na nastavení této události, než se pokusí odeslat odchozí zprávu.

  • Uzamkne CommunicationObject.ThisLock, aby zabránil změnám synchronizovaného stavu během odesílání. Další informace o stavech a stavovém počítači najdete v CommunicationObjectCommunicationObject dokumentaci.

  • Časový limit předaný funkci Send se použije jako časový limit pro celou operaci odesílání, která zahrnuje všechny části.

  • Vlastní návrh XmlDictionaryWriter byl vybrán, aby se zabránilo bufferování celého textu původní zprávy. Kdybychom aplikovali XmlDictionaryReader na tělo pomocí message.GetReaderAtBodyContents, bylo by celé tělo vyrovnáno do paměti. Namísto toho máme vlastní XmlDictionaryWriter, který je předán do message.WriteBodyContents. Když zpráva volá funkci WriteBase64 pomocí zapisovače, zapisovač segmenty zabalí do zpráv a odešle je prostřednictvím vnitřního kanálu. WriteBase64 blokuje, dokud není datový segment odeslán.

Implementace operace příjmu

Na vysoké úrovni operace Přijmout nejprve zkontroluje, zda příchozí zpráva není null a že její akce je ChunkingAction. Pokud nesplňuje obě kritéria, zpráva se vrátí beze změny z funkce Přijmout. Jinak funkce Receive vytvoří novou ChunkingReader a novou ChunkingMessage obálku kolem ní (voláním GetNewChunkingMessage). Před vrácením této nové ChunkingMessage Receive použije vlákno fondu vláken ke spuštění ReceiveChunkLoop, které volá innerChannel.Receive ve smyčce a předává bloky dat na ChunkingReader do doby, než se přijme zpráva o konci bloku nebo je dosažena doba vypršení časového limitu příjmu.

Několik podrobností stojí za zmínku:

  • Stejně jako funkce Odeslat, Přijmout nejprve zavolá ThrowIfDisposedOrNotOpened, abyste měli jistotu, že CommunicationState je otevřen.

  • Příjem je také synchronizovaný, takže z relace lze přijmout pouze jednu zprávu najednou. To je zvlášť důležité, protože jakmile se přijme počáteční zpráva bloku dat, očekává se, že všechny následné přijaté zprávy budou v této nové sekvenci bloků dat do přijetí zprávy koncového bloku. Příjem nemůže stahovat zprávy z vnitřního kanálu, dokud nejsou přijaty všechny části patřící k zprávě, která je aktuálně ve stavu de-chunkování. K tomuto účelu používá Receive objekt pojmenovaný ManualResetEventcurrentMessageCompleted, který je nastaven při přijetí zprávy koncového bloku a zresetován při přijetí nové zprávy počátečního bloku.

  • Na rozdíl od funkce Odeslat, příjem nezabrání přechodům synchronizovaného stavu při přijímání. Například během příjmu můžeme volat Close a čekat, dokud se nedokončí přijímání původní zprávy nebo dokud není dosaženo zadané hodnoty časového limitu.

  • Časový limit předaný službě Receive se používá jako časový limit pro celou operaci příjmu, která zahrnuje příjem všech bloků dat.

  • Pokud vrstva, která zprávu spotřebovává, spotřebovává text zprávy rychlostí nižší než rychlost příchozích bloků zpráv, ChunkingReader ukládá do vyrovnávací paměti tyto příchozí bloky dat až do limitu stanoveného ChunkingBindingElement.MaxBufferedChunks. Po dosažení limitu se z nižší vrstvy nepřebírají žádné další bloky dat, dokud není spotřebován blok ve vyrovnávací paměti nebo není dosažen časový limit příjmu.

Přepsání objektu CommunicationObject

OnOpen

OnOpen volá innerChannel.Open k otevření vnitřního kanálu.

OnClose

OnClose nejprve nastaví stopReceive na true, aby signalizoval čekajícímu ReceiveChunkLoop, že má ukončit. Pak vyčká na receiveStoppedManualResetEvent, které se nastaví, když se ReceiveChunkLoop zastaví. Pokud se ReceiveChunkLoop zastaví ve stanoveném časovém limitu, OnClose zavolá innerChannel.Close se zbývajícím časovým limitem.

OnAbort

OnAbort volá innerChannel.Abort aby přerušil vnitřní kanál. Pokud je ReceiveChunkLoop nevyřízeno, obdrží výjimku z čekajícího innerChannel.Receive volání.

OnFaulted

ChunkingChannel nevyžaduje zvláštní chování, když je kanál poruchový, takže OnFaulted není přepsán.

Implementace objektu pro vytváření kanálů

Zodpovídá ChunkingChannelFactory za vytváření instancí ChunkingDuplexSessionChannel a za kaskádové přechody stavu do objektu pro vytváření vnitřních kanálů.

OnCreateChannel používá vnitřní továrnu k vytvoření kanálu IDuplexSessionChannel. Pak vytvoří nový ChunkingDuplexSessionChannel a předá mu tento vnitřní kanál spolu se seznamem akcí zpráv, které mají být rozděleny do částí, a maximální počet částí, které se mají při příjmu ukládat do vyrovnávací paměti. Seznam akcí zpráv, které mají být rozděleny, a maximální počet částí ke zpracování ve vyrovnávací paměti jsou dva parametry předané ChunkingChannelFactory v jeho konstruktoru. Tato část ChunkingBindingElement popisuje, odkud tyto hodnoty pocházejí.

, OnOpenOnCloseOnAbort a jejich asynchronní ekvivalenty volají odpovídající metodu přechodu stavu ve vnitřní kanálu továrny.

Implementace naslouchacího rozhraní kanálů

ChunkingChannelListener je obal kolem posluchače vnitřního kanálu. Její hlavní funkcí, kromě delegování volání na toho vnitřního naslouchatele kanálu, je zabalit nové kanály ChunkingDuplexSessionChannels přijaté z vnitřního naslouchatele kanálu. To se provádí v OnAcceptChannel a OnEndAcceptChannel. Nově vytvořený ChunkingDuplexSessionChannel je předán vnitřnímu kanálu spolu s dalšími dříve popsanými parametry.

Implementace elementu vázání a vázání

ChunkingBindingElement je zodpovědný za vytvoření ChunkingChannelFactory a ChunkingChannelListener. Kontroluje ChunkingBindingElement , zda T v CanBuildChannelFactory<T> a CanBuildChannelListener<T> je typu IDuplexSessionChannel (jediný kanál podporovaný kanálem bloků dat) a že ostatní prvky vazby v vazbě podporují tento typ kanálu.

BuildChannelFactory <T> nejprve zkontroluje, zda je možné sestavit požadovaný typ kanálu, a poté získá seznam akcí pro zprávy, které se mají rozčlenit. Další informace najdete v následující části. Potom vytvoří nové ChunkingChannelFactory, kterému předá vnitřní továrnu kanálů (jak je vráceno z context.BuildInnerChannelFactory<IDuplexSessionChannel>), seznam akcí pro zpracování zpráv a maximální počet částí k ukládání do vyrovnávací paměti. Maximální počet bloků dat pochází z vlastnosti MaxBufferedChunks, kterou poskytuje ChunkingBindingElement.

BuildChannelListener<T> má podobnou implementaci pro vytvoření ChunkingChannelListener a předání vnitřnímu naslouchacímu kanálu.

V této ukázce je zahrnuta ukázková vazba s názvem TcpChunkingBinding. Tato vazba se skládá ze dvou prvků vazby: TcpTransportBindingElement a ChunkingBindingElement. Kromě zveřejnění vlastnosti MaxBufferedChunks vazba také nastaví některé z vlastností TcpTransportBindingElement, například MaxReceivedMessageSize (nastaví ji na ChunkingUtils.ChunkSize + 100 KB pro hlavičky).

TcpChunkingBinding také implementuje IBindingRuntimePreferences a vrací hodnotu true z metody ReceiveSynchronously, což označuje, že jsou implementována pouze synchronní volání Receive.

Určení, které zprávy se mají rozdělit na části

Kanál chunking segmentuje pouze zprávy identifikované prostřednictvím atributu ChunkingBehavior. Třída ChunkingBehavior implementuje IOperationBehavior a je implementována voláním AddBindingParameter metody. V této metodě ChunkingBehavior zkoumá hodnotu své AppliesTo vlastnosti (InMessage, OutMessage nebo obojí), aby určil, které zprávy mají být rozdělena na části. Pak získá akci každé z těchto zpráv (z kolekce Messages on OperationDescription) a přidá ji do kolekce řetězců obsažené v instanci ChunkingBindingParameter. Pak se ChunkingBindingParameter přidá do poskytnutého BindingParameterCollection.

To BindingParameterCollection se předá uvnitř BindingContext každého elementu vazby v vazbě, když tento element vazby sestaví objekt pro vytváření kanálů nebo naslouchací proces kanálu. Implementace ChunkingBindingElement a BuildChannelFactory<T> vytahuje tento BuildChannelListener<T> z BindingContext'BindingParameterCollection. Kolekce akcí obsažených v ChunkingBindingParameter je pak předána ChunkingChannelFactory nebo ChunkingChannelListener, která to následně předá ChunkingDuplexSessionChannel.

Spuštění ukázky

Nastavte, sestavte a spusťte ukázku

  1. Pomocí následujícího příkazu nainstalujte ASP.NET 4.0.

    %windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
    
  2. Ujistěte se, že jste pro ukázky windows Communication Foundation provedli jednorázovou instalační proceduru.

  3. Pokud chcete sestavit řešení, postupujte podle pokynů v části Ukázky sestavení služby Windows Communication Foundation.

  4. Pokud chcete spustit ukázku v jedno- nebo vícepočítačové konfiguraci, postupujte podle pokynů v Spuštění ukázek Windows Communication Foundation.

  5. Nejprve spusťte Service.exe a pak spusťte Client.exe a sledujte výstup v obou oknech konzoly.

Při spuštění ukázky se očekává následující výstup.

Klient:

Press enter when service is available

 > Sent chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd

Server:

Service started, press enter to exit
 < Received chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd