A Timely Example of a Service

I l @ ve RuBoard

As mentioned earlier, services are an excellent candidate for utility-style applications that run in the background and periodically poll data. This might include monitoring a process executing on the computer or on a network computer, or simply waiting for some other process or system to contact the service.

For example, let's assume that we need a way to synchronize the time of day on several machines. These machines reside on a private LAN and are not part of a domain and therefore can't reach out to the Internet to synchronize their time. These computers can, however, reach a machine on the network with access to the Internet. Let's create a time synchronization service, which we also included as the TimeService sample in the book's sample files. The sample uses the Network Time Protocol (NTP) to synchronize the time of the computer to another reference source somewhere on the Internet. The sample use a fairly well-known NTP host located at time.nist.gov . Let's examine the four primary functions of the service:

  • Communicate with the service

    Accept remote TCP connections on port 37 and upon connection respond with a 4-byte value representing the number of seconds since 1/1/1900, and then immediately close the connection. (This is basically what an NTP server does.)

  • Accept remoting calls through a predefined ITime interface

    This interface supports one method, which returns the current time.

  • Update the date and time

    Connect to an NTP time server once a day and update the time on the local machine.

  • Read from a local file a list of machines on the network that need to have their time updated periodically

    Synchronize the time on these machines with the time on the local machine running the service.

Communicating with the Service

One method of communicating with a service is to use the classes and methods contained in the System.Remoting namespace. Remoting simply allows for communication across application and application domain boundaries. Applications can reside on the same computer, the same LAN, or on networks on opposite sides of the world. Remoting uses a specified channel to transport messages to and from applications and uses formatters for encoding and decoding the messages before they're transported via the channel.

To see how extremely simple yet powerful remoting can be, take a look at the following code, which includes the OnStart method of the TimeService class:

 PublicClassTimeService InheritsSystem.ServiceProcess.ServiceBase Privatem_timeAsTime Privatem_tmrAsSystem.Timers.Timer Privatem_listenerAsThread ProtectedOverridesSubOnStart(ByValargs()AsString) 'Readtheremotingconfigurationfromthe<app>.exe.configfile. ' DimconfigFileAsString=Windows.Forms.Application.StartupPath&_  "\TimeService.exe.config" LogEvent("TheTimeServiceisStarting",_ EventLogEntryType.Information) Try m_time=NewTime() RemotingConfiguration.Configure(configFile) 'Startthethreadtolistenforconnectionsonport37 ' m_listener=NewThread(NewThreadStart(AddressOfListen)) 'Makesureitisabackgroundthreadsoitdoesn'tkeepthe 'processhangingaroundaftertheservicestops. ' m_listener.IsBackground=True m_listener.Name= "ListenerThreadcreatedat " &_ DateTime.Now.ToString() m_listener.Start() m_tmr=NewSystem.Timers.Timer(1000) AddHandlerm_tmr.Elapsed,AddressOfOnTimer m_tmr.Enabled=True CatchexAsException LogEvent("TheTimeServiceEncounteredanError: " &_ ex.ToString(),EventLogEntryType.Error) EndTry EndSub 

The OnStart method starts out by logging an event to the event log to show that the service is starting. The service then creates a new instance of a custom Time class (which we'll discuss in detail a little later). The next line is interesting: one line simply reads in everything the service needs in order to remote out an interface. The RemotingConfiguration class contains many static methods, such as the Configure method, that allows you to use XML to define your remoting infrastructure. It is highly recommend to use this feature if you want to communicate with a service via remoting. No recompiling is necessary to change the mode, channel, port, and so on ”you just restart your service. How cool is that!

 <?xmlversion="1.0" encoding="utf-8"?> <configuration> <system.runtime.remoting> <application> <service> <wellknownmode="Singleton"  type="TimeService.Time,TimeService"  objectUri="TimeServiceUri" /> </service> <channels> <channelref="tcp" port="9000" /> </channels> </application> </system.runtime.remoting> </configuration> 

In this service, we're remoting out the TimeService.Time type from the TimeService assembly as a singleton object, which is made available on a TCP channel at port 9000.

How do you get an instance of this object from outside of the application? We recommend that you create another Visual Basic .NET project and define your interfaces as shown in the following example. This is a fairly trivial example ”it contains only one method which clients will use to retrieve the current date and time.

 PublicInterfaceITime FunctionCurrentTime()AsDateTime EndInterface 

You then build this interface into its own assembly, named TimeLib.dll. This assembly contains only the definition for the ITime interface. The ITime interface is then implemented by the TimeService.Time class. The code for the ITime.CurrentTime method is shown here:

 'ImplementstheITime.CurrentTimemethodandreturnstheCurrentDateTime 'forthiscomputer ' PublicFunctionCurrentTime()AsDateImplementsITime.CurrentTime ReturnDateTime.Now() EndFunction 

Now for the client code. The first step is to reference the TimeLib.dll assembly we created above, and then the following function calls the static GetType method of the Type class to retrieve an instance of the ITime type. Next, we call Activator.GetObject , passing the ITime type and the URL to the server-activated well-known object. This URL is a concatenation of the protocol, server, channel, and objectURI we defined earlier in the TimeService application configuration file. Activator.GetObject creates a proxy for the well-known object which we cast to the ITime interface. From here, we can call methods directly on the interface.

 DimtimeAsITime DimtAsType=Type.GetType("TimeLib.ITime,TimeLib") IfNot(tIsNothing)Then time=CType(Activator.GetObject(t,_  "tcp://localhost:9000/TimeServiceUri"),ITime) If(timeIsNothing)Then MsgBox("CouldnotContactTimeService") Else MsgBox("TheTimeisCurrently: " &time.CurrentTime()) EndIf EndIf 

That's about it for basic communication via remoting. Remoting is covered in more detail in Chapter 6. The good thing about separating out the interface and placing it in its own assembly, is that you can simply deploy this assembly to any clients that need to communicate with the service. You can do this without having to deploy any implementation code for the underlying class that does all the work.

We can't verify that this service abides totally by the specification for an NTP server, so we won't call it one, but it does return 4 bytes that represent the number of seconds since January 1, 1900. So, looking back at the TimeService example, the OnStart method creates a new thread ( m_listener ) and points the ThreadStart at the Listen function:

 'ListensonPort37andSendsthenumberofelapsedseconds 'since1/1/1900 ' PublicSubListen() DimlistenerAsSocket DimsocketAsSocket DimhostAsIPEndPoint DimsecondsAsInteger Dimbuffer()AsByte Trace.WriteLine("ListenStarting") Try 'CreateaNewIPEndPointonport37forthelocalhost host=NewIPEndPoint(Dns.Resolve("127.0.0.1").AddressList(0),37) listener=NewSocket(AddressFamily.InterNetwork,_ SocketType.Stream,_ ProtocolType.Tcp) listener.Blocking=True 'BindtheListeningsockettothehostIPEndPoint listener.Bind(host) 'Allowupto10connectionstoqueueup. listener.Listen(10) 'CallAcceptwhichblockswaitingforaremoteconnection WhileTrue socket=listener.Accept() 'Ifwehaveavalidconnectionthensendthenumberofseconds 'since1/1/1900onthesocketandthencloseit. IfNot(socketIsNothing)Then Trace.WriteLine("Remotesocketconnectiononport37. " &_  "AddressFamily= " &_ socket.RemoteEndPoint.AddressFamily.ToString()) DimtsAsTimeSpan=DateTime.UtcNow.Subtract(_ NewDateTime(1900,1,1,0,0,0)) buffer=_ BitConverter.GetBytes(Convert.ToUInt32(ts.TotalSeconds)) Array.Reverse(buffer) socket.Send(buffer) socket.Shutdown(SocketShutdown.Both) socket.Close() EndIf EndWhile listener.Close() CatchexAsException LogEvent("AnexceptionoccurredinListen(): " &_ ex.ToString(),EventLogEntryType.Error) Finally socket.Close() listener.Close() EndTry EndSub 

The Listen function binds a listener socket to IPEndPoint of the localhost on port 37. It sets the socket to blocking and adjusts the size of the queue to handle up to 10 simultaneous connections. The service enters an infinite loop and blocks on the Accept call in the loop.

When a connection comes in, a socket is returned from the Accept method. After making sure the socket is still valid, the function calculates the number of seconds since 1/1/1900, stuffs the number into a 4-byte array, and sends the bytes out via the socket. Finally, it disables receiving and sending on this socket and then closes it out.

Updating the Date and Time

Now that we can communicate with our service, we need to make sure that our service maintains the correct date and time. Let's look again at the OnStart method for our service. It creates an instance of a server-based timer by instantiating an instance of the System.Timer class. Timers are the typical approach that most Windows services use to deal with monitoring or polling at specified intervals. It is simple to create a timer, set the interval, set up an event handler, and enable it.

Warning

The .NET Framework has two timers: the Windows Forms timer and the server-based timer in the System.Timers namespace. The Windows Forms timer is the familiar timer that is optimized for the user interface ”it requires a message pump in order to work. You should use the server-based timer in a Windows service because it has built-in thread safety and is optimized for the multithreaded environment.


The OnTimer event handler is triggered after the initial one second of life and then it adjusts the interval to every 24 hours.

 PublicSubOnTimer(ByValsourceAsObject,_ ByValeAsSystem.Timers.ElapsedEventArgs) 'Setthistoupdatethelocaltimeevery24hoursfromnowon m_tmr.Interval=86400000 Ifm_timeIsNothingThen m_time=NewTime() EndIf m_time.Update() UpdateClients() EndSub 

With the timer in place, let's take a look at the code for updating the time on the local machine. The following code connects to a well-known time server on the Internet, time.nist.gov , on port 37:

 PublicSubUpdate() DimtcpAsNewTcpClient() DimnetStreamAsNetworkStream Dimbuffer(3)AsByte DimbytesReadAsInteger DimsecondsAsLong DimdtimeAsDateTime Try 'Attempttoconnecttoawell-knownTimeServeronport37 tcp.Connect("time.nist.gov",37) netStream=tcp.GetStream() IfnetStream.CanReadThen 'Attempttoread4bytesfromtheConnection bytesRead=netStream.Read(buffer,0,4) If(bytesRead=4)Then 'Turnthisintothenumberofsecondssincethe1/1/1900 Array.Reverse(buffer) seconds=Convert.ToDouble(BitConverter.ToUInt32(buffer,0)) dtime=NewDateTime(1900,1,1,0,0,0) dtime=dtime.AddSeconds(seconds) 'Setthetimeonthelocalmachine m_localhost.SetDateTime(dtime.ToLocalTime) LogEvent("SuccessfullySetthetimeonthelocal" &_  " machineto " &dtime.ToLocalTime(),_ EventLogEntryType.Information) Else LogEvent("UnabletoSetthetimeonthelocal" &_  " machinefromservertime.nist.gov",_ EventLogEntryType.Error) EndIf EndIf CatchexAsException LogEvent(String.Format("UnabletoUpdatetheTimeon" &_  " theServer.Error:{0}",ex.Message),_ EventLogEntryType.Error) Finally 'Closethenetworkstream netStream.Close() EndTry EndSub 

Some of this code is like the code for our listener thread, except it converts the bytes back into seconds and then computes the date and time by adding the value (in seconds) to 1/1/1900. The code initiates a new instance of the TcpClient class, connects to the host on port 37, and gets the resulting network stream. Next, it reads the first 4 bytes off of the stream and verifies that the connection was successful. Finally, after the conversion to the DateTime object, the time in Universal Time Coordinate (UTC) format is converted to LocalTime before being passed to the SetDateTime method of the Machine object represented by the private class variable m_localhost .

The Machine class defines a method that takes a DateTime variable and uses Windows Management Instrumentation (WMI) to set the date and time on the particular computer. It does this by formatting the path , which includes the host name of the computer, and then passes it along to the newly created ManagementScope object. The following code shows this in action:

 PublicClassMachine Privatem_nameAsString PublicReadOnlyPropertyName() Get Returnm_name EndGet EndProperty PublicSubNew(ByValmachineNameAsString) m_name=machineName EndSub 'WMIWin32_OperatingSystemSetDateTimemethodexistsonlyon 'WindowsXP. ' PublicSubSetDateTime(ByValdtLocalAsDateTime) DimretvalAsSystem.UInt32 DimpathAsString=String.Format("\{0}\root\CIMV2",m_name) DimdtBeginAsDateTime=Now() DimtsElapsedAsTimeSpan DimosCollAsWMI.OperatingSystem.OperatingSystemCollection osColl=WMI.OperatingSystem.GetInstances(_ NewManagementScope(path), "") DimosAsWMI.OperatingSystem DimelapsedAsInteger ForEachosInosColl tsElapsed=Now.Subtract(dtBegin) dtLocal=dtLocal.Add(tsElapsed) retval=os.SetDateTime(dtLocal) IfSystem.Convert.ToInt32(retval)<>0Then Logger.LogEvent(String.Format(_  "ErrorSettingTimeon{0}.ErrorCode={1}",_ m_name,retval),EventLogEntryType.Error) EndIf Next EndSub EndClass 

WMI and Generating Management Strongly Typed Classes

The .NET Framework comes with a handy tool that generates source code for a strongly typed class from a particular WMI class. This tool makes it much easier to access properties and call functions defined within WMI classes.

For those of you that are new to WMI, here is a brief overview. WMI is a set of classes built into the Windows operating system. The classes make it easier for developers to author applications that monitor, manage, and detect failures in most aspects of the operating system, the computer, and applications running on the computer. Using WMI, you can retrieve a list of processes running on a machine, determine the free disk space on the primary partition, and retrieve a list of installed applications, among other tasks .

The syntax used to call WMI methods to obtain properties of WMI classes is typically not easy and can lead to errors. The mgmtclassgen.exe utility provided by the .NET Framework makes the code much simpler to write when accessing properties and invoking functions defined within the WMI classes. This utility has several command-line options; here are a few that was used to generate a file named os.vb which is included in the TimeService project:

 mgmtclassgenWin32_OperatingSystem/nroot\cimv2/lVB/pc:\os.vb 

Finally, we need to update the date and time on a set of clients from our computer hosting the service. In the OnTimer method, a call is made to the UpdateClients method:

 'Readsthemachinenamesfromtheclients.txtfile,createsanew 'Machineobjectinstanceforeachandaddsthemasaworkeritem 'toaThreadPool. ' PrivateSubUpdateClients() DimpathAsString=Windows.Forms.Application.StartupPath&_  "\clients.txt" DimrdrAsStringReader DimstrmAsStreamReader DimclientAsString DimmachineAsMachine 'Makesuretheclients.txtfileexists ' If(File.Exists(path))Then Trace.WriteLine("LoadingClientsfromfile " &path) Try 'OpentheTextfilereturningaStreamReaderandcreatea 'StringReadertoiterateovereachlineinthefile ' strm=File.OpenText(path) rdr=NewStringReader(strm.ReadToEnd()) 'Getthefirstlineinthefile ' client=rdr.ReadLine() DoWhileNot(clientIsNothing) 'Createanewmachineobjectfortheclientandqueueup 'anotherthreadtohandlesettingthetimeforthisclient. machine=NewMachine(client) ThreadPool.QueueUserWorkItem(AddressOfThreadFunc,machine) client=rdr.ReadLine() Loop CatchexAsException LogEvent("AnErroroccurredinUpdateClients: " &_ ex.ToString(),EventLogEntryType.Error) Finally rdr.Close() strm.Close() EndTry Else LogEvent(path& " doesnotexist.",_ EventLogEntryType.Information) EndIf EndSub 

This code is pretty straightforward. The service opens the clients.txt file and reads the entire file into a string variable. A StringReader then iterates through the file, grabbing each host name in the file, and creates a new a Machine object for it. The next line in the code queues a work item in the ThreadPool , passing each Machine object along for the ride.

The WaitCallBack delegate, shown in the following code, is called for every work item that hits the thread pool queue. This code casts the object to a Machine object and then calls the SetDateTime method on the instance, handling any exceptions along the way by logging to the event log.

 'CallBackFunctionwhichinitiatessettingthetimeoftheremote 'clientmachine ' PublicSubThreadFunc(ByValobjAsObject) DimclientAsMachine=CType(obj,Machine) Try LogEvent("SettingTimeonMachine " &client.Name,_ EventLogEntryType.Information) client.SetDateTime(DateTime.Now()) CatchexAsException LogEvent("AnExceptionwasThrownattemptingtoSettheTimeon " _ &client.Name& " Details: " &ex.ToString(),_ EventLogEntryType.Error) EndTry EndSub 

Finally, the OnStop event handler is defined in the service and is called when the service shuts down. The code in the OnStop event handler, disables the timer and logs an event to indicate that the service is shutting down.

 ProtectedOverridesSubOnStop() 'Addcodeheretoperformanytear-downnecessaryto 'stoptheservice. Timer1.Enabled=False LogEvent("TheTimeServiceisStopping",_ EventLogEntryType.Information) EndSub 
I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net