|
DCE RPC kennt ein Konzept namens „Pipes“. Die dahinterstehende Idee besagt, dass Client und Server für den effizienten Datentransfer einen Kanal zwischen sich aufbauen.
RPC Pipes verlangen allerdings einigen Aufwand bei der Programmierung, denn sie erfordern neben der Definition der Pipe und der zu transferierenden Daten auch Speicherallokierungs- und Deallokierungsfunktionen sowie den Code für den tatsächlichen Datentransfer. Dabei gilt es allerdings auseinanderzuhalten, dass RPC Pipes als RPC-Mechanismus definiert und nicht mit dem Netzwerkkonzept der Named Pipes zu verwechseln sind, auch wenn RPC als Layer über Named Pipes implementiert und Named Pipes für den Transport verwendet werden kann.
COM Pipes stehen unter Windows 2000 zur Verfügung und sind das COM-Äquivalent zu DCE Pipes. Sie finden Einsatz für den Transfer größerer Buffer mit BYTE, long oder double Werten, wofür COM drei Interfaces definiert: IPipeByte, IPipeLong und IPipeDouble. COM unterstützt den Marshalling code für diese Interfaces in OLE32.DLL. An der Oberfläche sehen diese Interfaces recht harmlos aus, wie nachfolgend am Beispiel von IPipeByte zu sehen ist. Mit Push() und Pull() werden lediglich zwei Methoden exportiert.
[ object,
uuid(DB2F3ACA-2F86-11d1-8E04-00C04FB9989A),
async_uuid(DB2F3ACB-2F86-11d1-8E04-00C04FB9989A),
pointer_default(unique)]
interface IPipeByte : IUnknown
{
HRESULT Pull( [out, size_is(cRequest),
length_is(*pcReturned)] BYTE* buf,
[in] ULONG cRequest,
[out] ULONG* pcReturned);
HRESULT Push( [in, size_is(cSent)] BYTE* buf,
[in] ULONG cSent);
}
Als Erstes fällt auf, dass Pipes sowohl synchron als auch asynchron arbeiten können. Ferner sind Pipes bi-direktional, so dass eine einmal aufgebaute Pipe sowohl Daten vom Pipe Implementor zum Datenkonsumer als auch vom Datenkonsumer zum Implementor übertragen kann. Ausgeführt werden die Transporte über die Methoden Pull(), mit der Daten angefordert werden und Push(), die Daten versendet.
Allerdings leisten Pipes noch mehr. Der Pipe Marshalling Code von Windows 2000 implementiert vorausschauendes Lesen. Dies bedeutet, dass COM, nachdem der Piper-Konsument einen Buffer vom Implementor erhalten hat, bereits nach dem nächsten Buffer Ausschau hält, bevor überhaupt eine neue Anforderung vorliegt.
Da in der Regel größere Datenmengen angefordert werden, kann man davon ausgehen, dass der Datenkonsument, nachdem er einen Buffer voll mit Daten erhalten hat, einige Zeit zur Verarbeitung benötigt, bevor die nächste Anforderung erfolgen kann. Die Übertragung des nächsten Datenbuffers über das Netz wird jedoch ebenfalls Zeit in Anspruch nehmen, so dass eine doppelte Laufzeiteinbuße für die Anwendung auftritt: Das erste Performanceloch entsteht durch die Verarbeitungszeit der Daten, während der die Datenquelle Idle ist, da keine weitere Anfrage erfolgt, das zweite entsteht dadurch, dass für die Dauer der Datenübertragung der Datenkonsument blockiert ist, da er nicht weiterarbeiten kann, bevor die Daten eingetroffen sind.
Der COM Pipe Marshalling Code umgeht die geschilderten Engpässe durch vorausschauendes Lesen, so dass während der Bearbeitung der angelieferten Daten durch den Konsumenten COM bereits die nächsten Daten von der Datenquelle anfordert, in der Erwartung, dass der Konsument ein weiteres Datenpaket anfordert, das COM dann bereits für den Konsumenten empfangen hätte und sofort übergeben könnte.
Ein solcher Mechanismus lässt sich natürlich auch auf Anwendungsebene unter Verwendung asynchroner, nicht blo- ckierender COM Calls realisieren, allerdings nicht so elegant.
Zu beachten ist dabei allerdings, dass COM nur den Marshalling Code bereitstellt, nicht jedoch die Implementation der tatsächlichen Pipes. Der Code für Pull() und Push() ist nach wie vor vom Entwickler zu schreiben.
Grundsätzlich gibt es zwei Möglichkeiten, Pipes einzusetzen, abhängig davon, ob die Pipes auf der Client- oder Serverseite implementiert werden. Implementiert der Client die Pipe, benötigt er ein Objekt, das eines der Pipe-Interfaces implementiert, das anschließend an die Serverkomponente in Form eines [in]-Parameters einer Interface-Methode übergeben wird.
Abbildung 5/2.7.6.1-1: Client implementiert das Pipe-Objekt und der Server ruft Methoden der Clientseite auf
Andererseits kann auch der Server die Pipe implementieren. In diesem Fall wird das Pipe-Interface über einen [out]-Zeiger an den Client übergeben, so dass der Client die Methoden der Pipe aufrufen kann.
Abbildung 5/2.7.6.1-2: Component implementiert das Pipe-Objekt, und die Clientaufrufe erfolgen auf der Serverseite
Zur Unterstützung des Konzepts der vorausschauenden Leseoperationen muss eine Pipe anzeigen, wenn keine weiteren Daten mehr zu übertragen sind. Dazu muss die Implementation der Methode Pull() die angeforderten Daten in cRequest übernehmen und die Anzahl der tatsächlich übertragenen Datenelemente im Parameter pcReturned mitteilen. COM nimmt dann weitere vorausschauende Aufrufe von Pull() vor, bis in pcReturned der Wert 0 anzeigt, dass keine weiteren Daten vorhanden sind. Dies bedeutet, dass immer ein zusätzlicher Aufruf an das Pipe-Interface notwendig ist, um das Ende der Übertragung festzustellen, da COM nicht automatisch annimmt, dass der Vergleich
*pcReturned != cRequest
für keine weiteren Daten steht. Dies bringt andererseits den Vorteil mit sich, dass nicht immer alle angeforderten Daten geliefert werden müssen. Dies wiederum verhilft der Pipe zu mehr Stabilität, da es durchaus sein kann, dass eine Datenquelle die Daten nicht so schnell bereitstellen kann, wie COM die Daten via Pull() anfordert.
Eine mögliche Implementation der Methode Pull() könnte wie nachfolgend beschrieben aussehen.
HRESULT CMyObj::Pull(BYTE* pBuf,
ULONG cRequest,
ULONG* pcReturned){
if (bNoMoreData)
{
*pcReturned = 0;
return S_OK;
}
*pcReturned = GetDataFromSomewhere(pBuf, cRequest);
return S_OK;}
Hierbei ist die Methode GetDataFromSomewhere() eine benutzerdefinierte Methode innerhalb der Datenquelle, die die zu übertragenden Daten erzeugt oder bereitstellt.
Im Datenkonsument kann die Abfrage in Form einer Schleife implementiert werden, wobei die nachfolgend eingesetzte benutzerdefinierte Methode ProcessData() hier für das Verarbeiten der Daten zuständig ist. In den Pull()-Aufrufen werden jeweils Blöcke zu 1.000 Bytes angefordert.
|