Loosely Coupled Transactor Client [1]
IntentProvide asynchronous services to those systems whose installed clients can be edited or created without the need for .NET on the server or client, or a messaging system of any kind. These clients include Windows 2000 or Windows XP without the .NET runtime. Utilize multithreading to mimic asynchronous behavior on a client. ProblemThe reason for implementing this pattern is that, like the LCT Server, an application can greatly benefit from adding asynchronous-type behavior to a design and can do so in a nonintrusive manner. Fortunately, just as the LCT Server above is a simplified "afterthought," the LCT Client provides the developer with a client-side "add-on" that should not disrupt an existing client implement. However, unlike the LCT Server, this pattern is typically deployed on the client and may require more deployment time. Despite this limitation, this offers a highly controllable means of calling Web services (or any external call) in a loosely coupled fashion. This also provides a solution for those installed applications that do not have the option of changing the server or calling a Web service and do not have .NET installed on the client. Finally, it does not require any messaging software to work. ForcesUse the LCT Server pattern when:
StructureJust like the LCTServer pattern, the structure begins with any client (does not have to be .NET) that wishes to execute a synchronous operation a synchronously. The class diagram (Figure 7.7) shows this as being the LCTClientMain entity. In the sample code, this is a simple console application written in Visual C++ 7.0. It has two main purposes. First it provides an implementation of a generic callback function whose pointer is passed to the LCTClient during preparation and later called during execution. Second, it calls the Web service using an instantiated object declared as a type, employing a generated ATL template. The ATL template is generated from SPROXY.EXE (Visual C++ 7.0) and provides a custom ATL-based proxy by which to call any Web service method from the client. The details of the implementation are in the following section. Other than that, most of the work is done by the ATL framework, the custom CLCTClientSoapSocket class, and the template it uses, called CSoapSocketClientT . Figure 7.7. LCT Client generic class diagram.
Another feature used in this pattern is that of a ThreadPool object. Although one cannot effectively implement this pattern without the use of mutithreading, employing an actual "thread pool" is optional. However, once again, ATL does most of the work, so why not ? The ThreadPool object is just like any other object used by the LCTClientMain to create and initialize the pool and add each Web service proxy object to it, just like any array objects. The core of this pattern is rather simple but it's the technology behind it that makes this a more advanced pattern implementation. This pattern acts as hybrid between an architecture and an implementation pattern, due to its technological particularities.
ConsequencesThe LCT Client pattern has the following benefits and liabilities:
Participants
ImplementationOne of the primary benefits of this pattern is its ability to be seamlessly added to an efficient asynchronous callback mechanism for any client. However, this pattern does require some understanding beyond just that of C# or VB.NET. For those aching to get their hands dirty with C++, this can be great way to dive in. To start things off, let's first look the LCTClientMain console application (Listing 7.17). Listing 7.17 LCT Client C++ console implementation.#include "stdafx.h" #include "ExternalService.h" #include "LCTClient.h" #define CLIENTS_COUNT 8 struct stCallbackParam { int iServerIndex; LPLONGlpCompletionCounter; }; /* //This is the callback that is registered with the LCTClient // class during PrepareAsync */ void LCTCallback(LPARAM lParam, HRESULT hRet) { stCallbackParam*param= reinterpret_cast<stCallbackParam*>(lParam); ATLASSERT(param); // increment the shared counter InterlockedIncrement(param->lpCompletionCounter); // do something useful with the HRESULT printf("\n[Client %d] - Async Result : %s", param->iServerIndex, SUCCEEDED(hRet)?"SUCCEEDED":"FAILED"); } /* // This just drives the LCT Client to execute GetTransaction from // an external web service pooling up // several clients in the process. The number of clients is // configurable. */ int _tmain(int argc, _TCHAR* argv[]) LCTClientThreadPoolthreadPool; tCallbackParam paramsExternalService[CLIENTS_COUNT]; BSTR bstrRet; int iIndex = 0; int iPoolSize = CLIENTS_COUNT; HRESULT hRet; LONG lCompletionCounter = 0; int nCallCounter = 0; CoInitialize(NULL); // Initialize the tread pool to 4 threads per CPU threadPool.Initialize(0, -4); { // allocate an array of the number of pooled // clients you want CExternalServiceT<CLCTClientSoapSocket> <- no line break proxiesExternalService[CLIENTS_COUNT]; // loop through each imaginary client, prepare request, and // call asynchronously using LCT client for heavy lifting for(iIndex = 0; iIndex < iPoolSize; iIndex++) { // server index can be replaced by some unique // cookie/handle for async ops paramsExternalService[iIndex].iServerIndex = iIndex + 1; paramsExternalService[iIndex].lpCompletionCounter = &lCompletionCounter; // pass in an initialized threadpool (the number is // up to you), the callback and the parameters hRet = proxiesExternalService[iIndex]. PrepareRequest(&threadPool, LCTCallback, (LPARAM)¶msExternalService[iIndex]); ATLASSERT(SUCCEEDED(hRet)); // just call the sample GetTransaction Web service // method, the param will be the return // value OUT of that call hRet = proxiesExternalService[iIndex]. GetTransaction(&bstrRet); // if E_PENDING things are working.. ATLASSERT(E_PENDING == hRet); } // Request in work, yet do { Sleep(50); printf("."); } while(lCompletionCounter < CLIENTS_COUNT); // call callbacks have been called we can continue and // receive the operation return values for(iIndex=0; iIndex<CLIENTS_COUNT; iIndex++) { printf("\nClient %d : ", iIndex + 1); // Back from the request // Collect the results now // Pass only the OUT and IN/OUT params // The rest will be ignored hRet = proxiesExternalService[iIndex]. GetTransaction(&bstrRet); // now really analyze the results if(SUCCEEDED(hRet)) { nCallCounter++; printf("Requests Succeeded. Result :\n %ls", bstrRet); ::SysFreeString(bstrRet); } else printf("Request Failed"); } if(nCallCounter == CLIENTS_COUNT) { ::MessageBox(NULL, _T("All async call s completed."), _T("Status"),MB_OK); } } // make sure and clean up the threadpool printf("\n"); threadPool.Shutdown(); CoUninitialize(); return 0; } As discussed in the Participants section, any working client can replace this entity. This is only a sample driver of the pattern. Its primary duties are to declare the ExternalServiceT template by passing in the LCTClientSoapSocket class, intializing the thread pool, calling PrepareRequest, executing the Web service iteratively, and implementing a callback to be called upon completion of the original transaction. How this is implemented exactly is up to the developer. The real work happens in the CLCTClientSoapSocket class in Listing 7.18. Listing 7.18 LCT Client "plumbing" implementation.#include <atlsoap.h> #define LCTCLIENT_EXECUTION 101 class CLCTClientSoapSocket; typedef void (*SOAP_ASYNC_CALLBACK)(LPARAM lParam, HRESULT hRet); structstLCTClientRequest { CLCTClientSoapSocket *pClient; SOAP_ASYNC_CALLBACK pfnCallback; LPARAM lpRequestParam; }; class CLCTClientWorker { public: typedef stLCTClientRequest* RequestType; CLCTClientWorker() throw() { } ~CLCTClientWorker() throw() { } virtual BOOL Initialize(void *pvParam) throw() { // Do any initialization here return TRUE; } virtual void Terminate(void* pvParam) throw() { // Do any cleanup here } void Execute(stLCTClientRequest *pRequestInfo, void *pvParam, OVERLAPPED *pOverlapped) throw(); }; typedef CThreadPool<CLCTClientWorker>CLCTClientThreadPool; class CLCTClientSoapSocket : public CSoapSocketClientT<> { protected: stLCTClientRequest m_stRequestInfo; bool m_bInRequest; HRESULT m_hRealRet; CLCTClientThreadPool*m_pThreadPool; CStringm_strSOAPAction; public: CLCTClientSoapSocket(LPCTSTR szUrl) : CSoapSocketClientT<>(szUrl) { m_bInRequest = false; m_pThreadPool = NULL; } // Note : this is not thread safe, the caller should take // care HRESULT PrepareRequest(CLCTClientThreadPool *pThreadPool, SOAP_ASYNC_CALLBACK pfnCallback, LPARAM lParam) { // return false, if already in request if(m_bInRequest) return E_PENDING; if(!pThreadPool) return E_POINTER; m_pThreadPool = pThreadPool; m_stRequestInfo.pClient = this; m_stRequestInfo.lpRequestParam= lParam; m_stRequestInfo.pfnCallback=pfnCallback; m_bInRequest = true; return S_OK; } HRESULT SendRequest(LPCTSTR szAction) { HRESULT hRet; if(m_bInRequest) { // launch the async request m_strSOAPAction = szAction; ATLASSERT(m_pThreadPool); if(m_pThreadPool->QueueRequest(&m_stRequestInfo)) hRet = E_PENDING; else hRet = E_FAIL; } else { // post-async request, reading the response. // Nothing to do, just return hRet from the // real operation hRet = m_hRealRet; } return hRet; } HRESULT InternalSendRequest() { m_hRealRet = CSoapSocketClientT<>::SendRequest(m_strSOAPAction); m_bInRequest = false; return m_hRealRet; } }; inline void CLCTClientWorker::Execute(stLCTClientRequest *pRequestInfo, void *pvParam, OVERLAPPED *pOverlapped) throw() { ATLASSERT(pRequestInfo != NULL); pvParam; // unused pOverlapped; // unused CLCTClientSoapSocket *pClient = pRequestInfo->pClient; SOAP_ASYNC_CALLBACK pfnCallBack = pRequestInfo->pfnCallback; LPARAM lParam = pRequestInfo->lpRequestParam; ATLASSERT(pClient); HRESULT hRet = pClient->InternalSendRequest(); if(pfnCallBack) { pfnCallBack(lParam, hRet); } } This piece of code is rather simple and mainly leverages the use of "thread pools" (see Visual Studio .NET documentation for details on thread pooling) for delegating each synchronous call from the pool onward to the Web service. When a send request is called, what would normally be a synchronous call, instead of immediately calling GetTransaction(), the call is queued using the Thread Pool object. Once on the queue, it will be later called by the Thread worker, using CLCTClientWork::Execute() (Listing 7.18). From here, all the work has already been performed in relation to this pattern, and it is simply delegated to SoapSocketClient::SendRequest() that sends the transaction as it would during a normal synchronous execution. To see how the LCTClient class is passed into the generated ExternalServiceT proxy, you have to look at the generated code to appreciate what is going on. In Listing 7.19, you'll find the generated ATL template for the ExternalService Web service that contains GetTransaction() and any other method exposed publicly from the Web service. Listing 7.19 LCT Client "Plumbing" Implementation “ Template Code.#include <atlsoap.h> struct TransactorHeader { __int64 lTransactorId; }; template <typename TClient = CSoapSocketClientT<> > class CExternalServiceT : public TClient, public CSoapRootHandler { public: // // SOAP headers used for passed transaction id for ease // TransactorHeader TransactorHeader0; protected: const _soapmap ** GetFunctionMap(); const _soapmap ** GetHeaderMap(); void * GetHeaderValue(); const wchar_t * GetNamespaceUri(); const char * GetServiceName(); const char * GetNamespaceUriA(); HRESULT CallFunction(void *pvParam, const wchar_t *wszLocalName, int cchLocalName, size_t nItem); HRESULT GetClientReader(ISAXXMLReader **ppReader); public: . . . CExternalServiceT(ISAXXMLReader *pReader = NULL) :TClient(_T("http://localhost/. . ./ExternalService/ExternalService.asmx")) { SetClient(true); SetReader(pReader); // // initialize headers // memset(&TransactorHeader0, 0x00, sizeof(TransactorHeader)); } ~CExternalServiceT() { Uninitialize(); } void Uninitialize() { UninitializeSOAP(); // // uninitialize headers // AtlCleanupValueEx(&TransactorHeader0, GetMemMgr()); } HRESULT InvokeTimeConsumingMethod(BSTR* InvokeTimeConsumingMethodResult); HRESULT GetTransaction(BSTR* GetTransactionResult); }; typedef CExternalServiceT<> CExternalService; __if_not_exists(__TransactorHeader_entries) { extern __declspec(selectany) const _soapmapentry __TransactorHeader_entries[] = { { 0xC0792CDA, "lTransactorId", L"lTransactorId", sizeof("lTransactorId")-1, SOAPTYPE_LONG, SOAPFLAG_FIELD, offsetof(TransactorHeader, lTransactorId), NULL, NULL, -1 }, { 0x00000000 } }; extern __declspec(selectany) const _soapmap __TransactorHeader_map = { 0x8AA088EA, "TransactorHeader", L"TransactorHeader", sizeof("TransactorHeader")-1, sizeof("TransactorHeader")-1, SOAPMAP_STRUCT, __TransactorHeader_entries, sizeof(TransactorHeader), 1, -1, SOAPFLAG_NONE, 0xC2E575C3, "http://tempuri.org/", L"http://tempuri.org/", sizeof("http://tempuri.org/")-1 }; } struct __CExternalService_InvokeTimeConsumingMethod_struct { BSTR InvokeTimeConsumingMethodResult; }; . . . struct __CExternalService_GetTransaction_struct { BSTR GetTransactionResult; }; extern __declspec(selectany) const _soapmapentry __CExternalService_GetTransaction_entries[] = { { 0xE7350D25, "GetTransactionResult", L"GetTransactionResult", sizeof("GetTransactionResult")-1, SOAPTYPE_STRING, SOAPFLAG_NONE SOAPFLAG_OUT SOAPFLAG_PID SOAPFLAG_DOCUMENT SOAPFLAG_LITERAL SOAPFLAG_NULLABLE, offsetof(__CExternalService_GetTransaction_struct, GetTransactionResult), NULL, NULL, -1, }, { 0x00000000 } }; extern __declspec(selectany) const _soapmap __CExternalService_GetTransaction_map = { 0x88540AB5, "GetTransaction", L"GetTransactionResponse", sizeof("GetTransaction")-1, sizeof("GetTransactionResponse")-1, SOAPMAP_FUNC, __CExternalService_GetTransaction_entries, sizeof(__CExternalService_GetTransaction_struct), 1, -1, SOAPFLAG_NONE SOAPFLAG_PID SOAPFLAG_DOCUMENT SOAPFLAG_LITERAL, 0xC2E575C3, "http://tempuri.org/", L"http://tempuri.org/", sizeof("http://tempuri.org/")-1 }; extern __declspec(selectany) const _soapmap * __CExternalService_funcs[] = { &__CExternalService_InvokeTimeConsumingMethod_map, &__CExternalService_GetTransaction_map, NULL }; // cleanup any in/out-params and out-headers from previous calls Cleanup(); __atlsoap_hr = BeginParse(__atlsoap_spReadStream); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_PARSE_ERROR); goto __cleanup; } . . . template <typename TClient> inline HRESULT CExternalServiceT<TClient>::GetTransaction(BSTR* GetTransactionResult) { HRESULT __atlsoap_hr = InitializeSOAP(NULL); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_INITIALIZE_ERROR); return __atlsoap_hr; } CleanupClient(); CComPtr<IStream> __atlsoap_spReadStream; __CExternalService_GetTransaction_struct __params; memset(&__params, 0x00, sizeof(__params)); __atlsoap_hr = SetClientStruct(&__params, 1); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_OUTOFMEMORY); goto __skip_cleanup; } __atlsoap_hr = GenerateResponse(GetWriteStream()); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_GENERATE_ERROR); goto __skip_cleanup; } __atlsoap_hr = SendRequest(_T("SOAPAction: \"http:// . . ./GetTransaction\"\r\n")); if (FAILED(__atlsoap_hr)) { goto __skip_cleanup; } __atlsoap_hr = GetReadStream(&__atlsoap_spReadStream); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_READ_ERROR); goto __skip_cleanup; } // cleanup any in/out-params and out-headers from previousCleanup(); __atlsoap_hr = BeginParse(__atlsoap_spReadStream); if (FAILED(__atlsoap_hr)) { SetClientError(SOAPCLIENT_PARSE_ERROR); goto __cleanup; } *GetTransactionResult = __params.GetTransactionResult; goto __skip_cleanup; __cleanup: Cleanup(); __skip_cleanup: ResetClientState(true); memset(&__params, 0x00, sizeof(__params)); return __atlsoap_hr; } template <typename TClient> ATL_NOINLINE inline const _soapmap ** CExternalServiceT<TClient>::GetFunctionMap() { return __CExternalService_funcs; } . . . static const _soapmapentry __CExternalService_GetTransaction_atlsoapheader_entries[] = { { 0x00000000 } }; static const _soapmap __CExternalService_GetTransaction_atlsoapheader_map = { 0x88540AB5, "GetTransaction", L"GetTransactionResponse", sizeof("GetTransaction")-1, sizeof("GetTransactionResponse")-1, SOAPMAP_HEADER, __CExternalService_GetTransaction_atlsoapheader_entries, 0, 0, -1, SOAPFLAG_NONE SOAPFLAG_PID SOAPFLAG_DOCUMENT SOAPFLAG_LITERAL, 0xC2E575C3, "http://tempuri.org/", L"http://tempuri.org/", sizeof("http://tempuri.org/")-1 }; static const _soapmap * __CExternalService_headers[] = { . . . &__CExternalService_GetTransaction_atlsoapheader_map, NULL }; return __CExternalService_headers; } template <typename TClient> ATL_NOINLINE inline void * CExternalServiceT<TClient>::GetHeaderValue() { return this; } template <typename TClient> ATL_NOINLINE inline const wchar_t * CExternalServiceT<TClient>::GetNamespaceUri() { return L"http://tempuri.org/"; } template <typename TClient> ATL_NOINLINE inline const char * CExternalServiceT<TClient>::GetServiceName() { return NULL; } template <typename TClient> ATL_NOINLINE inline const char * CExternalServiceT<TClient>::GetNamespaceUriA() { return "http://tempuri.org/"; } template <typename TClient> ATL_NOINLINE inline HRESULT CExternalServiceT<TClient>::CallFunction(void *, const wchar_t *, int, size_t) { return E_NOTIMPL; } template <typename TClient> ATL_NOINLINE inline HRESULT CExternalServiceT<TClient>::GetClientReader(ISAXXMLReader **ppReader) { if (ppReader == NULL) { return E_INVALIDARG; } CComPtr<ISAXXMLReader> spReader = GetReader(); if (spReader.p != NULL) { *ppReader = spReader.Detach(); return S_OK; } return TClient::GetClientReader(ppReader); } Although this pattern utilizes ATL and C++ and can be considered advanced for those with primarily C# or VB.NET skills, the generic applicability of this implementation allows any Web service to be represented, threaded, and executed. With the new ATL features, threading and thread pooling have never been easier. This provides the conduit within which to build any asynchronous model using client code. As long as the clients are relatively low in number and simple in complexity, this pattern can be applied to many areas. For those who do not yet have .NET installed, it can provide a tremendous bridge from the "old unmanaged world" to the "new world" of .NET. |