Segmentierungskanal
Das Beispiel für den ChunkingChannel zeigt, wie mit einem benutzerdefinierten Protokollkanal oder Mehrschicht-Kanal das Segmentieren und Desegmentieren beliebig großer Nachrichten vorgenommen werden kann.
Beim Senden großer Nachrichten mithilfe von Windows Communication Foundation (WCF) ist es häufig sinnvoll, die Menge des zur Pufferung dieser Nachrichten verwendeten Arbeitsspeichers zu begrenzen. Eine mögliche Lösung besteht im Streamen des Nachrichtentexts (vorausgesetzt, der größte Teil der Daten befindet sich dort). Einige Protokolle erfordern jedoch die Pufferung der Nachricht als Ganzes. Zuverlässiges Messaging und Sicherheit sind zwei solche Beispiele. Eine weitere mögliche Lösung besteht darin, die große Nachricht in kleinere Nachrichten zu teilen, so genannte Segmente, diese Segmente jeweils einzeln zu senden und dann auf der Empfängerseite die große Nachricht wiederherzustellen. Die Anwendung selbst könnte diese Segmentierung und Desegmentierung vornehmen, oder es könnte alternativ ein benutzerdefinierter Kanal dafür verwendet werden.
Die Segmentierung sollte stets nur dann eingesetzt werden, wenn die gesamte Nachricht, die gesendet werden soll, erstellt wurde. Ein Segmentierungskanal sollte immer unter einem Sicherheitskanal und einem zuverlässigen Sitzungskanal angeordnet sein.
Hinweis
Die Setupprozedur und die Buildanweisungen für dieses Beispiel befinden sich am Ende dieses Themas.
Segmentierungskanal Voraussetzungen und Einschränkungen
Nachrichtenstruktur
Der Segmentierungskanal geht bei zu segmentierenden Nachrichten von folgender Nachrichtenstruktur aus:
<soap:Envelope>
<!-- headers -->
<soap:Body>
<operationElement>
<paramElement>data to be chunked</paramElement>
</operationElement>
</soap:Body>
</soap:Envelope>
Bei Verwendung von ServiceModel erfüllen Vertragsvorgänge mit 1 Eingabeparameter diese Nachrichtenform bei ihrer Eingabenachricht. Ebenso erfüllen Vertragsvorgänge mit 1 Ausgabeparameter bzw. Rückgabewert diese Nachrichtenform bei ihrer Ausgabenachricht. Es folgen Beispiele für solche Vorgänge:
[ServiceContract]
interface ITestService
{
[OperationContract]
Stream EchoStream(Stream stream);
[OperationContract]
Stream DownloadStream();
[OperationContract(IsOneWay = true)]
void UploadStream(Stream stream);
}
Sitzungen
Der Segmentierungskanal erfordert, dass Nachrichten genau einmal zugestellt werden, und zwar in geordneter Zustellung von Einzelnachrichten (Segmenten). Dies bedeutet, dass der zugrunde liegende Kanalstapel sitzungsbasiert sein muss. Sitzungen können vom Transport bereitgestellt werden (z. B. TCP-Transport) oder durch einen sitzungsbasierten Protokollkanal (z. B. ReliableSession-Kanal).
Asynchrones Senden und Empfangen
Asynchrone Methoden zum Senden und Empfangen sind in dieser Version des Segmentierungskanalbeispiels nicht implementiert.
Segmentierungsprotokoll
Der Segmentierungskanal definiert ein Protokoll, das den Start und das Ende einer Segmentreihe sowie die laufende Nummer jedes Segments angibt. Die folgenden drei Beispielnachrichten veranschaulichen die Start-, Segmentierungs- und Endnachricht mit Kommentaren, in denen jeweils die wichtigsten Aspekte der einzelnen Elemente beschrieben werden.
Startnachricht
<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>
Segmentierungsnachricht
<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>
Endnachricht
<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>
Segmentierungskanalarchitektur
Der Segmentierungskanal ist ein IDuplexSessionChannel
, der im Allgemeinen der typischen Kanalarchitektur folgt. Es gibt ein ChunkingBindingElement
, das eine ChunkingChannelFactory
und einen ChunkingChannelListener
erstellen kann. ChunkingChannelFactory
erstellt auf Anforderung Instanzen von ChunkingChannel
. ChunkingChannelListener
erstellt Instanzen von ChunkingChannel
, wenn ein neuer innerer Kanal akzeptiert wird. Der ChunkingChannel
selbst ist für das Senden und Empfangen von Nachrichten verantwortlich.
Eine Ebene tiefer beruht ChunkingChannel
für die Implementierung des Segmentierungsprotokolls auf mehreren Komponenten. Auf der Senderseite verwendet der Kanal einen benutzerdefinierten XmlDictionaryWriter, den so genannten ChunkingWriter
, der die eigentliche Segmentierung durchführt. ChunkingWriter
verwendet den inneren Kanal direkt zum Senden von Segmenten. Durch die Verwendung eines benutzerdefinierten XmlDictionaryWriter
können Segmente versendet werden, während der große Text der ursprünglichen Nachricht geschrieben wird. Dies bedeutet, dass nicht die gesamte ursprüngliche Nachricht gepuffert wird.
Auf der Empfängerseite ruft ChunkingChannel
Nachrichten aus dem inneren Kanal ab und gibt sie an einen benutzerdefinierten XmlDictionaryReader, den so genannten ChunkingReader
weiter, der die ursprüngliche Nachricht wieder aus den eingehenden Segmenten zusammensetzt. ChunkingChannel
bindet diesen ChunkingReader
in einer benutzerdefinierten Message
-Implementierung, der so genannten ChunkingMessage
, ein und gibt diese Nachricht an die darüberliegende Schicht zurück. Diese Kombination aus ChunkingReader
und ChunkingMessage
ermöglicht die Desegmentierung des ursprünglichen Nachrichtentexts, während dieser von der darüberliegenden Schicht gelesen wird. Es ist also nicht erforderlich, den gesamten Text der ursprünglichen Nachricht zu puffern. ChunkingReader
enthält eine Warteschlange, in der die eingehenden Segmente bis zu der maximal konfigurierbaren Anzahl gepufferter Segmente gepuffert werden. Wenn diese Obergrenze erreicht ist, wartet der Leser, bis Nachrichten durch die darüberliegende Schicht aus der Warteschlange abfließen (d. h. einfach durch Lesen aus dem ursprünglichen Nachrichtentext) oder bis das maximale Empfangs-Timeout erreicht ist.
Programmierungsmodell für die Segmentierung
Dienstentwickler können angeben, welche Nachrichten segmentiert werden sollen, indem sie das Attribut ChunkingBehavior
auf Vorgänge innerhalb des Vertrags anwenden. Das Attribut macht eine AppliesTo
-Eigenschaft verfügbar, mit der der Entwickler angeben kann, ob sich die Segmentierung auf die Eingabenachricht, die Ausgabenachricht oder auf beides bezieht. Im folgenden Beispiel wird die Verwendung des ChunkingBehavior
-Attributs veranschaulicht.
[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);
}
Aus diesem Programmierungsmodell kompiliert ChunkingBindingElement
eine Liste von Aktions-URIs, die die zu segmentierenden Nachrichten identifizieren. Die Aktion der einzelnen ausgehenden Nachrichten wird mit dieser Liste verglichen, um zu bestimmen, ob die Nachricht segmentiert oder direkt gesendet werden soll.
Implementieren des Sendevorgangs
Allgemein gesagt überprüft der Sendevorgang zunächst, ob die ausgehende Nachricht segmentiert werden muss, und falls nicht, sendet sie die Nachricht direkt über den inneren Kanal.
Wenn die Nachricht segmentiert werden muss, erstellt Send einen neuen ChunkingWriter
und ruft WriteBodyContents
in der ausgehenden Nachricht auf, der sie diesen ChunkingWriter
übergibt. Der ChunkingWriter
nimmt dann die Nachrichtensegmentierung vor (dazu gehört das Kopieren der ursprünglichen Nachrichtenheaders in die Startsegmentnachricht) und sendet Segmente über den inneren Kanal.
Hier einige Details, die beachtet werden sollten:
Send ruft zunächst
ThrowIfDisposedOrNotOpened
auf, um sicherzustellen, dassCommunicationState
geöffnet ist.Das Senden wird synchronisiert, sodass für die einzelnen Sitzungen jeweils nicht mehrere Nachrichten gleichzeitig gesendet werden können. Es gibt einen
ManualResetEvent
namenssendingDone
, der zurückgesetzt wird, wenn eine segmentierte Nachricht gesendet wird. Sobald die Endsegmentnachricht gesendet wurde, ist dieses Ereignis festgelegt. Die Send-Methode wartet, bis dieses Ereignis festgelegt ist, bevor sie versucht, die ausgehende Nachricht zu senden.Send sperrt
CommunicationObject.ThisLock
, um Änderungen am synchronisierten Zustand während des Sendens zu vermeiden. Weitere Informationen zu CommunicationObject-Zuständen und den Zustandsautomaten finden Sie in der CommunicationObject-Dokumentation.Der an Send übergebene Timeout dient als Timeout für den gesamten Sendevorgang, der das Senden aller Segmente umfasst.
Der benutzerdefinierte XmlDictionaryWriter-Entwurf wurde ausgewählt, um zu vermeiden, dass der gesamte Text der ursprünglichen Nachricht gepuffert wird. Wenn mithilfe von XmlDictionaryReader ein
message.GetReaderAtBodyContents
für den Text verwendet würde, würde der gesamte Nachrichtentext gepuffert. Stattdessen haben wir einen benutzerdefinierten XmlDictionaryWriter, der anmessage.WriteBodyContents
übergeben wird. Wenn die Nachricht WriteBase64 beim Writer aufruft, fasst der Writer Segmente zu Nachrichten zusammen und sendet sie über den inneren Kanal. WriteBase64 blockiert, bis das Segment gesendet wurde.
Implementieren des Empfangsvorgangs (Receive)
Allgemein gesagt stellt der Empfangsvorgang (Receive) zunächst sicher, dass die eingehende Nachricht nicht null
und dass ihre Aktion ChunkingAction
lautet. Wenn sie nicht beide Kriterien erfüllt, wird die Nachricht unverändert von Receive zurückgegeben. Andernfalls erstellt Receive einen neuen ChunkingReader
und eine neue ChunkingMessage
, die ihn umgibt (durch Aufrufen von GetNewChunkingMessage
). Vor der Rückgabe dieser neuen ChunkingMessage
führt Receive mithilfe eines Threadpool-Threads ReceiveChunkLoop
aus, wodurch wiederum innerChannel.Receive
in einer Schleife aufgerufen wird und Segmente an ChunkingReader
übergeben werden, bis die Endsegmentnachricht empfangen oder der Receive-Timeout erreicht wird.
Hier einige Details, die beachtet werden sollten:
Wie Send ruft auch Receive zunächst
ThrowIfDisposedOrNotOpened
auf, um sicherzustellen, dassCommunicationState
geöffnet ist.Auch Receive wird synchronisiert, sodass aus der Sitzung nicht mehrere Nachrichten gleichzeitig empfangen werden können. Dies ist besonders wichtig, da nach dem Empfang einer Startsegmentnachricht davon ausgegangen wird, dass alle anschließend empfangenen Nachrichten Segmente innerhalb dieser neuen Segmentsequenz sind, bis eine Endsegmentnachricht empfangen wird. Receive kann so lange keine Nachrichten mithilfe von Pull aus dem inneren Kanal übertragen, bis alle Segmente, die zu der Nachricht gehören, die momentan wieder aus ihren Segmenten zusammengesetzt wird, empfangen wurden. Um dies zu erreichen, verwendet Receive ein
ManualResetEvent
namenscurrentMessageCompleted
, das beim Empfang der Endsegmentnachricht festgelegt und beim Empfang einer neuen Startsegmentnachricht zurückgesetzt wird.Im Gegensatz zu Send verhindert Receive während des Empfangs keine Zustandsübergänge bei synchronisierten Zuständen. So kann beispielsweise Close während des Empfangs aufgerufen werden. Der Vorgang wartet dann, bis der ausstehende Empfang der ursprünglichen Nachricht abgeschlossen bzw. der angegebene Timeoutwert erreicht ist.
Der an Receive übergebene Timeout dient als Timeout für den gesamten Empfangsvorgang, der den Empfang aller Segmente umfasst.
Wenn die Schicht, die die Nachricht verwendet, den Nachrichtentext langsamer verwendet als Segmentnachrichten eintreffen, puffert
ChunkingReader
die eingehenden Segmente bis zu der vonChunkingBindingElement.MaxBufferedChunks
angegebenen Obergrenze. Nachdem dieser Grenzwert erreicht wurde, werden erst dann wieder Segmente mithilfe von Pull aus der unteren Schicht übertragen, wenn entweder ein gepuffertes Segment verwendet oder der Empfangs-Timeout erreicht wurde.
CommunicationObject-Überschreibungen
OnOpen
OnOpen
ruft innerChannel.Open
auf, um den inneren Kanal zu öffnen.
OnClose
OnClose
legt zuerst stopReceive
auf true
fest, um zu signalisieren, dass der ausstehendeReceiveChunkLoop
beendet werden soll. Es wartet dann auf das receiveStopped
ManualResetEvent, das festgelegt wird, wenn ReceiveChunkLoop
beendet wird. Angenommen, ReceiveChunkLoop
wird innerhalb des angegebenen Timeouts beendet, dann ruft OnClose
mit dem restlichen Timeout innerChannel.Close
auf.
OnAbort
OnAbort
ruft innerChannel.Abort
auf, um den inneren Kanal abzubrechen. Wenn es einen ausstehenden ReceiveChunkLoop
gibt, erhält diese eine Ausnahme vom ausstehenden innerChannel.Receive
-Aufruf.
OnFaulted
Der ChunkingChannel
erfordert kein besonderes Verhalten, wenn der Kanal einen Fehler verursacht hat, sodass OnFaulted
nicht überschrieben wird.
Implementieren einer Kanalfactory
Der ChunkingChannelFactory
ist dafür zuständig, Instanzen von ChunkingDuplexSessionChannel
zu erstellen und Zustandsübergänge an die innere Kanalfactory weiterzugeben.
OnCreateChannel
verwendet die innere Kanalfactory zur Erstellung eines inneren IDuplexSessionChannel
-Kanals. Anschließend erstellt es einen neuen ChunkingDuplexSessionChannel
, dem sie beim Empfang diesen inneren Kanal sowie die Liste der zu segmentierenden Nachrichtenaktionen und die maximale Anzahl der zu puffernden Segmente übergibt. Die Liste der zu segmentierenden Nachrichtenaktionen und die maximale Anzahl der zu puffernden Segmente sind zwei Parameter, die im Konstruktor an ChunkingChannelFactory
weitergegeben werden. Im Abschnitt über ChunkingBindingElement
wird beschrieben, woher diese Werte kommen.
OnOpen
, OnClose
, OnAbort
und ihre asynchronen Entsprechungen rufen die entsprechende Zustandsübergangsmethode in der inneren Kanalfactory auf.
Implementieren eines Kanallisteners
Der ChunkingChannelListener
ist ein Wrapper um einen inneren Kanallistener. Neben dem Delegieren von Aufrufen an diesen inneren Kanallistener besteht seine Hauptaufgabe darin, neue ChunkingDuplexSessionChannels
um Kanäle zu legen, die aus dem inneren Kanallistener akzeptiert wurden. Dies erfolgt in OnAcceptChannel
und OnEndAcceptChannel
. Dem neu erstellten ChunkingDuplexSessionChannel
wird der innere Kanal, zusammen mit den anderen, bereits beschriebenen Parametern, übergeben.
Implementieren von Bindungselement und Bindung
ChunkingBindingElement
ist für das Erstellen der ChunkingChannelFactory
und des ChunkingChannelListener
verantwortlich. ChunkingBindingElement
überprüft, ob T in CanBuildChannelFactory
<T> und CanBuildChannelListener
<T> den Typ IDuplexSessionChannel
aufweist (der einzige Kanal, der vom Segmentierungskanal unterstützt wird) und ob die anderen Bindungselemente der Bindung diesen Kanaltyp unterstützen.
BuildChannelFactory
<T> überprüft zunächst, ob der angeforderte Kanaltyp erstellt werden kann, und ruft anschließend eine Liste der zu segmentierenden Nachrichtenaktionen auf. Weitere Informationen finden Sie im folgenden Abschnitt. Anschließend erstellt es einen neuen ChunkingChannelFactory
, dem es die innere Kanalfactory (wie aus context.BuildInnerChannelFactory<IDuplexSessionChannel>
zurückgegeben), die Liste der Nachrichtenaktionen und die maximale Anzahl der beim Empfang zu puffernden Segmente übergibt. Die maximale Anzahl an Segmenten stammt aus einer Eigenschaft namens MaxBufferedChunks
, die vom ChunkingBindingElement
verfügbar gemacht wird.
BuildChannelListener<T>
hat zum Erstellen von ChunkingChannelListener
und zur Übergabe an den inneren Kanallistener eine ähnliche Implementierung.
In diesem Beispiel ist eine Beispielbindung namens TcpChunkingBinding
enthalten. Diese Bindung besteht aus zwei Bindungselementen: TcpTransportBindingElement
und ChunkingBindingElement
. Die Bindung macht nicht nur die Eigenschaft MaxBufferedChunks
verfügbar, sondern legt auch einige der TcpTransportBindingElement
-Eigenschaften fest, wie beispielsweise MaxReceivedMessageSize
(wird auf ChunkingUtils.ChunkSize
+ 100 KB für Header festgelegt).
TcpChunkingBinding
implementiert außerdem IBindingRuntimePreferences
und gibt den Wert true aus der ReceiveSynchronously
-Methode zurück, was anzeigt, dass nur die synchronen Receive-Aufrufe implementiert werden.
Bestimmen der zu segmentierenden Nachrichten
Der Segmentierungskanal segmentiert nur die Nachrichten, die über das ChunkingBehavior
-Attribut identifiziert wurden. Die ChunkingBehavior
-Klasse implementiert IOperationBehavior
und wird durch Aufrufen der AddBindingParameter
-Methode implementiert. In dieser Methode untersucht ChunkingBehavior
den Wert dieser AppliesTo
-Eigenschaft (InMessage
, OutMessage
oder beides), um zu bestimmen, welche Nachrichten segmentiert werden sollen. Anschließend ruft es die Aktion jeder diese Nachrichten ab (aus der Nachrichtenauflistung unter OperationDescription
) und fügt sie einer Zeichenfolgenauflistung hinzu, die in einer Instanz von ChunkingBindingParameter
enthalten ist. Anschließend wird dieser ChunkingBindingParameter
der angegebenen BindingParameterCollection
hinzugefügt.
Diese BindingParameterCollection
wird innerhalb von BindingContext
an die einzelnen Bindungselemente in der Bindung übergeben, wenn das betreffende Bindungselement die Kanalfactory oder den Kanallistener erstellt. Die ChunkingBindingElement
-Implementierung von BuildChannelFactory<T>
und BuildChannelListener<T>
ruft diesen ChunkingBindingParameter
aus dem BindingParameterCollection
von BindingContext'
ab. Die Auflistung der in ChunkingBindingParameter
enthaltenen Aktionen wird anschließend an ChunkingChannelFactory
oder ChunkingChannelListener
übergeben, von wo aus sie wiederum an ChunkingDuplexSessionChannel
übergeben wird.
Ausführen des Beispiels
So können Sie das Beispiel einrichten, erstellen und ausführen
Installieren Sie ASP.NET 4.0 mithilfe des folgenden Befehls.
%windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
Stellen Sie sicher, dass Sie die Beispiele zum einmaligen Setupverfahren für Windows Communication Foundation ausgeführt haben.
Befolgen Sie zum Erstellen der Projektmappe die Anweisungen unter Erstellen der Windows Communication Foundation-Beispiele.
Wenn Sie das Beispiel in einer Konfiguration mit einem Computer oder über Computer hinweg ausführen möchten, folgen Sie den Anweisungen unter Durchführen der Windows Communication Foundation-Beispiele.
Führen Sie zuerst Service.exe und dann Client.exe aus, und sehen Sie sich die Ausgabe in beiden Konsolenfenstern an.
Beim Ausführen des Beispiels sollte die Ausgabe wie folgt aussehen.
Client:
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