Server to Server Asynchronous Communications

Sometimes the time between request and response will be really long. The processing may take a long time, a person may need to do some work before returning a result, or it just might not make sense to wait until the call completes. For example, when placing a purchase order, the client may only care that the order was received. Later, the server can notify the client of the following events:

  • Payment processed

  • Order being processed

  • Order shipped

  • Changes in order location up through delivery

These changes may happen over several days, making it difficult to keep any connections alive. Instead of keeping a connection alive, why not send the request and response over independent connections? To do this, both ends of the SOAP message exchange need to be able to listen for another computer initiating a connection. The other requirement is that the server needs to know what the response message should look like.

In general, you will need to have a SOAP server on each end. Each server implements a different Web Service. When a request goes out, the client sends a one-way message to the server. The message should have at least two pieces of information associated through method arguments or SOAP Header information—the URL to send the response to and an identifier that the client can use to match the response with the original request. The identifier is very important to have, because the client may send several messages before the server gets around to responding. The client needs to implement a Web Service interface with which the server knows how to work. The way of doing this that makes the most sense to me is to have the server define that interface and require clients to implement the interface.

Because the time between request and response may be great, it may make sense for the actual Web Service to store the essential data from the requests for processing by a client-side or server-side application. Why would you do this? On the server side, if the transaction is long lived, you do not want to tie up the Web endpoint while it processes the request. Instead of launching thread in IIS, you may be better off processing the request in another process.

For the client side, the client that issued the request may not be active when the response comes back. Here again, the Web Service handling the response should store the results in a place where the client will know to look.

Because we have given a brief overview of when you would want to implement a Web Service capable of returning results later and viewed a few of the requirements for what the server and client would need to do, let's look at an example—the “Hello World” of disconnected asynchronous processing.

This application consists of two Web Services—a console application and a WinForm application. You can obtain the source for these applications from the companion Web site. The applications live in the following projects:

  • Client Web Service: Server1

  • Server Web Service: Server2

  • Client Application: ServiceClient

  • Server Application: ServiceProcessor

To run the example, you will need to have Microsoft Message Queue installed. We will start with the server Web Service.

Server Web Service

The Server Web Service is implemented by the RequestService class. This class has one Sub, HelloWorld. HelloWorld takes a unique identifier and a URL to which to send the response. It stores this information in a queue for the console application to process at a later time. Listing 8.7 shows this class in detail.

Listing 8.7. The RequestService Class (Server2RequestService.asmx.vb)
 1:  Imports System.Web.Services
 2:  Imports System.Web.Services.Protocols
 3:  Imports System.Messaging
 4:
 5:  <WebService(Namespace:= _
 6:      "http://scottseely.com/Request/RequestService.asmx"), _
 7:   SoapDocumentService()> _
 8:  Public Class RequestService
 9:      Inherits System.Web.Services.WebService
10:
11:      ' The name of the queue that this Web Service reads.
12:      Private Const QUEUE_NAME As String = _
13:          ".Private$Ch8HelloWorldRequest"
14:
15:      <WebMethod(), _
16:          SoapDocumentMethod(OneWay:=True)> _
17:      Public Sub HelloWorld(ByVal theGUID As Guid, _
18:          ByVal theURL As String)
19:          Dim theQueue As MessageQueue
20:
21:          ' Open up the request queue and place the
22:          ' notification there. The client application knows
23:          ' to listen on that endpoint.
24:          If Not (MessageQueue.Exists(QUEUE_NAME)) Then
25:              theQueue = MessageQueue.Create(QUEUE_NAME)
26:          Else
27:              theQueue = New MessageQueue(QUEUE_NAME)
28:          End If
29:
30:          theQueue.Send(theURL, theGUID.ToString())
31:          theQueue.Close()
32:
33:      End Sub
34:
35:  End Class

The HelloWorld Web Method is designed to not return a response. Line 16 instructs .NET that any callers should not to wait for a response. When WSDL is generated, the operation element will only contain an input member. This also tells the underlying framework to close the connection after the request is received and not to return anything back to the client. When the request has been received, all information pertaining to the request is saved in a private queue. The actual work will occur in the console application. If you are following this model, you will want to do something similar and store the data for the method call for another application to process.

Lines 17 and 18 of listing 8.7 show the Web Service taking two string values as arguments. The first value, theGUID, is stored for later use in mapping the result to the original request. The second value, theURL, tells the server where to send any response. For a simple example or intranet application, this is good enough. The sender can be trusted and worries about abuse are fairly low.

If this same approach is used to give external clients access to the Web Service, the Web Method should do some extra work before having any work done for the message. To do this, you could make sure that the caller and the host name in the response URL mapped to the same computer. If mapping to one computer does not make sense, because the client could be behind a firewall and have its address obscured or because of deployment issues, such as many clients using one Web Server to store responses, the preceding verification would not be a good option. Instead, the server could maintain a list of clients that it will respond to in a database, configuration file, or other location. When a request comes in, the response URL would be validated against that table. This solution could be expanded to map client address ranges to specific response URLs. It would also mean that the server could throw away any requests not coming from a valid IP address. To work, this type of validation would have to be set up before the client accessed the Web Service.

Client Web Service

Before covering the console application that processes a request, I would like to show the Web Service that handles any responses. The Web Service “client” implements a class named ResponseService. ResponseService is almost an exact copy of the RequestService class. The main differences are that the second parameter contains a result instead of a URL, and the response is stored to a different queue.

Listing 8.8. The ResponseService Class (Server1ResponseService.asmx.vb)
 1:  Imports System.Web.Services
 2:  Imports System.Web.Services.Protocols
 3:  Imports System.Messaging
 4:
 5:  <WebService(Namespace:= _
 6:      "http://scottseely.com/Response/ResponseService.asmx"), _
 7:   SoapDocumentService()> _
 8:  Public Class ResponseService
 9:      Inherits System.Web.Services.WebService
10:
11:      ' The name of the queue that this Web Service reads.
12:      Private Const QUEUE_NAME As String = _
13:          ".Private$Ch8HelloWorldResponse"
14:
15:      <WebMethod(), _
16:          SoapDocumentMethod(OneWay:=True)> _
17:      Public Sub ReturnHelloWorld(ByVal theGUID As Guid, _
18:          ByVal theResponse As String)
19:
20:          ' Open up the response queue and place the
21:          ' notification there. The client application knows
22:          ' to listen on that endpoint.
23:          Dim theQueue As MessageQueue
24:          If Not (MessageQueue.Exists(QUEUE_NAME)) Then
25:              theQueue = MessageQueue.Create(QUEUE_NAME)
26:          Else
27:              theQueue = New MessageQueue(QUEUE_NAME)

28:          End If
29:
30:          theQueue.Send(theResponse, theGUID.ToString())
31:          theQueue.Close()
32:
33:      End Sub
34:
35:  End Class

Like the server-side Web Service, this Web Method supports one-way messaging. It uses the GUID in the return message as a way to map the original request to the response. Because the response sits in a queue, the client can process the message when it is ready.

Console Application

The console application reads the messages from the request queue and responds to each request. In a production environment, this application probably would be written as a Windows Service. In this example, I use a console application so that I can easily show what is happening to the user. Listing 8.9 shows the complete listing for the server-side message handler. In lines 18–27, the application handles initialization. After that little bit of works is complete, the application sits and waits for messages to arrive. Message processing happens between lines 30 and 61. The console blocks on line 38 wait for messages to arrive. When a message does arrive, the application simulates work by waiting five seconds on line 44 before sending the response to the client Web Service. The wait is important if you are running the client and the server at the same time. When the wait is absent, the response comes back immediately and more or less ruins the effect of the sample. If you want to see the quick turnaround, just comment out line 44 and re-run the example on your machine.

Listing 8.9. The Module1 Class (ServiceProcessorModule1.vb)
 1:  Imports System.Messaging
 2:
 3:  Module Module1
 4:
 5:      ' The name of the queue the app reads from
 6:      Private Const QUEUE_NAME As String = _
 7:          ".Private$Ch8HelloWorldRequest"
 8:
 9:      Sub Main()
10:          Dim theQueue As MessageQueue
11:          Dim theMsg As Message
12:          Dim svc As New localhost.ResponseService()
13:          Dim theResponse As String
14:          Dim theBody As Object
15:
16:          ' If the queue exists, create it. Otherwise,
17:          ' simply open it.
18:          If Not (MessageQueue.Exists(QUEUE_NAME)) Then
19:              theQueue = MessageQueue.Create(QUEUE_NAME)
20:          Else
21:              theQueue = New MessageQueue(QUEUE_NAME)
22:          End If
23:
24:          ' Tell the queue object how to read data
25:          ' out and what it should place the type into.
26:          theQueue.Formatter = New XmlMessageFormatter(New Type() _
27:                  {GetType([String])} )
28:
29:          ' Run forever.
30:          While True
31:
32:              Try
33:                  ' Tell the user how to quit
34:                  Console.WriteLine("Press CTRL-C to stop listening")
35:
36:                  ' Wait synchronously for the next message in the
37:                  ' queue.
38:                  theMsg = theQueue.Receive()
39:
40:                  ' Read the body in as a string.
41:                  theBody = CType(theMsg.Body, [String])
42:
43:                  ' Simulate work
44:                  System.Threading.Thread.Sleep(5000)
45:
46:                  ' Create the response and send it to the
47:                  ' client's Web Service.
48:                  svc.Url = theBody.ToString()
49:                  theResponse = "Message returned at " & _
50:                      DateTime.Now.ToString()
51:                  svc.ReturnHelloWorld(New Guid(theMsg.Label), _
52:                     _theResponse)
53:              Catch ex As Exception
54:                  ' The Url probably did not work.
55:                  Console.WriteLine(ex.ToString())
56:              End Try
57:              ' Show the user that something happened on
58:              ' this end.
59:              Console.WriteLine("Just processed " & theMsg.Label & _
60:                  vbCrLf & theBody.ToString())
61:          End While
62:          theQueue.Close()
63:      End Sub
64:
65:  End Module
						

Lines 59 and 60 send some output to the console. In particular, they print the GUID that was just processed and the destination URL. The output for one message exchange is shown in Figure 8.3

Figure 8.3. Figure 8.3 console application processing three messages from the client.


While testing this console application, I tried filling the queue up with hundreds of messages before running the console. After seeing how slow the processing was going, I decided to try running several instances of the console application. Of course, the request queue started shrinking faster and faster. Due to the highly disconnected nature of this architecture, its use gives a developer great scalability options. If the machines processing messages read from a common queue or other data source, it should be pretty easy to scale out by adding extra hardware as needed whenever request levels exceed the capabilities of the current hardware. Yes, this is something that queues allow for, and I thought it was pretty neat that even this simple example can show the benefit.

WinForm Application

The WinForm application sends all requests and processes all responses. Listing 8.10 shows the code that does all the work for the WinForm. The form sends requests whenever its button is clicked. The request itself is pretty simple. On lines 10 and 11, HelloWorld is called and a new Guid is created. In a real world implementation, the Guid would be saved somewhere to map the request message to the response message.

Every second, a timer event occurs. When that happens, the response queue is opened and the code checks to see if any responses are in the queue. Lines 43–47 peek into the queue and check if any messages are waiting. When a message is found, the code pulls the response out and displays the Guid and response message in a list box.

Listing 8.10. The WinForm Client (ServiceClientForm1.vb)
 1:  ' The name of the queue we check for messages.
 2:  Private Const QUEUE_NAME As String = _
 3:      ".Private$Ch8HelloWorldResponse"
 4:
 5:  Private Sub Button1_Click(ByVal sender As System.Object, _
 6:      ByVal e As System.EventArgs) Handles btnSendRequest.Click
 7:
 8:      ' Execute the "HelloWorld" of inter-endpoint async
 9:      Dim svc As New WindowsApplication1.localhost.RequestService()
10:      svc.HelloWorld(Guid.NewGuid(), _
11:          "http://localhost/Server1/ResponseService.asmx")
12:
13:      ' Let the user that the message went out already.
14:      MsgBox("Test Succeeded", 0, Nothing)
15:
16:  End Sub
17:
18:  Private Sub tmrCheckQueue_Tick(ByVal sender As System.Object, _
19:      ByVal e As System.EventArgs) Handles tmrCheckQueue.Tick
20:
21:      ' Stop the timer while handling this event
22:      tmrCheckQueue.Enabled = False
23:      Dim theQueue As MessageQueue
24:      Dim theMsg As Message
25:      Dim waitTime As New TimeSpan(100)
26:
27:      ' Check the queue. If it doesn't exist,
28:      ' create it.
29:      If Not (MessageQueue.Exists(QUEUE_NAME)) Then
30:          theQueue = MessageQueue.Create(QUEUE_NAME)
31:      Else
32:          theQueue = New MessageQueue(QUEUE_NAME)
33:      End If
34:
35:      ' Tell the queue how to read values
36:      theQueue.Formatter = New XmlMessageFormatter(New Type() _
37:          {GetType([String])} )
38:
39:      Try
40:          ' If any messages are in the queue, read them
41:          ' and place the body text in the Response
42:          ' listbox.
43:          While Not (theQueue.Peek(waitTime) Is Nothing)
44:              theMsg = theQueue.Receive()
45:              Me.lbResponses.Items.Add("GUID: " & theMsg.Label & _
46:                  " " & CType(theMsg.Body, [String]))
47:          End While
48:      Catch ex As MessageQueueException
49:          ' This is expected whenever no messages are
50:          ' sitting in the queue.
51:      End Try
52:
53:      ' Close the queue.
54:      theQueue.Close()
55:      tmrCheckQueue.Enabled = True
56:  End Sub
						

Figure 8.4 shows the client application after reading the responses generated when Figure 8.3 was captured. Visually, you can match the request and response messages.

In situations where the client and the server can both host a Web Service and when a near term response is not needed, you can achieve asynchronous messaging by using pairs of one-way messages. When using this model, it seems to work best when the Web Service stores the request for another application to process. Likewise, the client needs to have a Web Service that can listen for responses. Then, the client can periodically check to see results have returned. The server needs to cooperate by persisting the unique identifier and by being able to use the client-side Web Service. The client-side identifier only needs to be unique enough. A monotonically increasing number would work just as well as a GUID. The example used a GUID only because it is globally unique, limiting the probability of a mismatched request/response pair.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset