Developing Smart Device WiFi Applications with the .NET Compact Framework
JW Hedgehog, Inc.
Microsoft .NET Compact Framework 1.0
Microsoft Visual Studio® .NET 2003
Summary: Learn to create a Multi-Communication Framework that allows .NET Compact Framework and Full .NET Framework applications to share information in a WiFi friendly way.
Using the Framework
Creating the Client
Creating the Listener
Smart Device applications are becoming increasingly complex. The day of the stand-alone smart device application has passed. Modern smart device applications must share information with peer devices and desktops in near real-time and therefore must take advantage of available network connectivity.
Making an application network aware always has its challenges but even more so when dealing with smart device applications. These applications do not have the reliability of being on a wired network. Instead they must use wireless networks (WiFi) and must be tolerant to its sometimes-connected-sometimes-not nature.
To address this need, I have put together a .NET Compact Framework library called the MultiCommFramework. The MultiCommFramework allows .NET Compact Framework and Full .NET Framework applications to share information in a WiFi friendly way.
At a high-level, the MultiCommFramework is modeled after the concept of multicasting. In regular multicasting, client applications register with the network router interest in information sent to a particular multicast address. Anytime information is sent to that address, the router distributes it to all registered clients. The biggest issue with multicasting is that the router must support multicasting and the router must have multicasting enabled. In many smaller networks this is not the case. It is also possible that network administrators may choose to restrict access to multicasting.
Instead of relying on the router, in the MultiCommFramework one instance of the application accepts the role of the Listener (a.k.a. Host). This application instance is responsible for tracking the list of registered clients and distributing information sent by any one of the clients to the others. In addition to the distribution responsibilities, the listener also acts as a client able to send and receive messages just as any other client. Just like in multicasting, the MultiCommFramework utilizes connectionless UDP datagrams to avoid the problems associated with trying to maintain a connection-oriented protocol in the loosely connected environment of WiFi. The MultiCommFramework works equally well with the .NET Compact Framework and the full .NET Framework
In addition to the MultiCommFramework, the accompanying solution includes two sample applications: MultiCommChat and MultiCommChat_DT. Both applications act as a simple chat program and are implemented with the MultiCommFramework. MultiCommChat is a smart device project and MultiCommChat_DT is a desktop version of the same chat program. Although it would've been possible to share code between the two projects, all code is explicitly implemented in each to make the code easy to follow. By default the MultiCommChat program acts as the listener/host while the MultiCommChat_DT acts as a simple client.
You can perform a simple demonstration of the MultiCommFramework by launching the MultiCommChat on a network enabled smart device and MultiCommChat_DT on your desktop. To start the conversation, choose Conversation/Join from the menu on each. You can then enter messages and tap/press "Send" to send them to each other. When done, choose Conversation/Leave or just exit the programs.
Note The MultiCommFramework relies on UDP broadcasts to initiate conversations. The Pocket PC emulator does not properly handle UDP broadcasts so as a result, the MultiCommFramework must be run using an actual device and cannot be run on the emulator.
As mentioned in the introduction, the Framework relies on one instance of an application to act as the Listener and for all other applications instances to be a client. The individual roles are determined by which class is created. Clients create and utilize the MultiCommClient class while the listener instance creates and utilizes the MultiCommListener class. The MultiCommListener inherits from MultiCommClient and in most cases allows the application to perform the listener specific behavior in a single routine at object creation time. Once created, the MultiCommListener can be down-cast to a MultiCommClient allowing a common code path for client and listener instances. The MultiCommChat and MultiCommChat_DT projects demonstrate this behavior.
Utilizing the framework is fairly straightforward as the communications details are fully encapsulated. An application must decide on two communication ports and a conversation name. One port is used by clients to listen for incoming information. The other port is used by the listener to receive information from the clients. The conversation name is used as an identifier to verify that a message belongs to a specific application conversation. The conversation name can be any arbitrary string and must be the same for all applications involved in the conversation. One key point is that you must insure that exactly one application instance acts as the host for a conversation name/port combination. With no listener, content cannot be distributed. With multiple listeners there will be fragmentation of the conversation.
The life of a client is pretty simple; connect up to a listener then just send a receive messages. This behavior is encapsulated in the MultiCommClient class; to use the MultiCommClient, do the following:
- Instantiate an instance of MultiCommClient. The constructor expects a string identifying the conversation and an integer port number on which it will listen for incoming messages.
- Connect to the appropriate events. See the "Handling Events" section for details.
- Call the Start method. This launches a background thread that listens for incoming messages.
- At this point you can just wait for a listener to invite you into a conversation. If a listener invites you, it will also identify its IP Address and the port on which it is listening so the framework will automatically connect the client up to the listener.
- Instead of just waiting for a listener to send out an invitation whenever it's ready, you can call the SendInviteRequest method. This method sends a broadcast to see if there is a listener already running that handles this conversation. If there is a listener, it immediately invites this client into the conversation. Unless you have some specific reason for letting the listener decide when it would like to invite new clients, you should call this method right after calling start. Although this method does not require you to know the IP Address of the listener, you will need to pass the port on which you expect the listener to be listening.
- Once connected into a conversation (the client can know by either handling the OnJoin event or checking the IsInConversation property), the client can now send messages and will receive messages sent by other clients.
The listener is responsible for distributing information to the client instances of the application. This behavior is handled by the MultiCommListener class; to use the MultiCommListener do the following:
- Instantiate an instance of MultiCommListener. The constructor expects a string identifying the conversation, a integer port number on which clients are listening and a integer port on which the listener will listen for client messages
- Connect to appropriate events. See the "Handling Events" section for details.
- Call the Start method. This launches a background thread that listens for information coming from clients and a background thread that distributes messages received by one client to the others.
- In most cases, you will also want to call the SendInvitation method. This broadcasts an invitation for any running client instances to connect to this listener. These clients will now be able to share information
- Once we're up and running, most applications will down-cast the MultiCommListener instance to a MultiCommClient reference. This allows the bulk of the application to perform the same whether acting as a client or listener.
- Whether as a MultiCommListener or downcast to a MultiCommClient, the listener can now send messages and will be notified when clients send messages.
Events notify your application of activity within the MultiCommFramework. The only event you need to handle is the OnReceive event. This event is at the heart of the MultiCommFramework as it's the mechanism the framework uses to notify the application that a message has been received. The handling of the other events is completely up to you as they indicate MultiCommFramework system activity but require no action on the part of the application.
Most of the message processing of the MultiCommFramework occurs on background threads. For efficiency, the events are fired by the thread on which they occur For this reason, you must be careful about interacting directly with the user interface in any of these event handlers as interacting with the user interface from a background thread creates application instability. Be sure to use the Control.Invoke pattern to interact with the user interface to avoid this problem (see MultiCommChat event handlers for an example).
Here's the list of MultiCommFramework events. Note that OnJoin and OnLeave contain slightly different information depending upon whether they are fired by a client or listener.
|OnReceive||Indicates that a client message has been received. The event will contain the byte array of data that one of the clients has passed to the Send method. Any client calling Send will result in every other registered client (and the listener) having an OnReceive fired. |
This event is at the heart of the MultiCommFramework because this is how the framework notifies your application that another client has sent you information. This is the only message that indicates application-level activity. All others fire as a result of MultiCommFramework system functionality and are informational only.
|OnJoin||Indicates that a client has successfully joined the conversation. This event is fired on the specific client who has joined the conversation and on the listener. For the client this event contains the conversation name and the IP Address of the listener. For the listener, this event contains the conversation name and the IP Address of the client that just joined.|
|OnLeave||Indicates that a client has left the conversation and occurs as a result of a client calling the Stop method. This event is fired on the specific client who has left the conversation and on the listener. For the client this event contains the conversation name and the IP Address of the listener. For the listener, this event contains the conversation name and the IP Address of the client that just left.|
|OnTerminate||Indicates that the listener is terminating the conversation and occurs as a result of the listener calling the Stop method. No further messages from any client will be distributed once the listener terminates the conversation. The event contains the conversation name.|
The MultiCommFramework solution contains two sample projects: MultiCommChat and MultiCommChat_DT. These projects are a simple chat program and demonstrate how to use the MultiCommFramework. They are smart device and desktop versions respectively of the same program containing nearly identical source code with the exception of a few user interface differences.
To see a simple demonstration of the applications and therefore the MultiCommFramework, do the following:
- Launch MultiCommChat on a network enabled smart device and MultiCommChat_DT on your network enabled desktop.
- Choose Conversation/Join on both. The order doesn't matter as the listener will automatically search for existing clients and the client will automatically search for an existing listener. You should see messages on each indicating that they have started and connected.
- Enter messages into each and tap/press send. Repeat this as often as you like.
- Choose Conversation/Leave on one then the other. You should see messages indicating the conversation is being left or terminated.
This is the simplest use of the MultiCommFramework; one instance being the listener and one instance being a client. By default the smart device instance is set to be the listener. You can change this by going to Conversation/Options and unchecking the "Act as conversation host" checkbox. Remember there must always be exactly one listener in a conversation.
You'll also notice that the chat programs are showing user names with each message. This is something introduced by the chat application and has no meaning within the MulitCommFramework. The MultiCommFramework tracks members via their IP Address.
The chat programs provide complete control over all of the MultiCommFramework settings via Conversation/Options. You can change the client and/or listener port numbers and the conversation name. Just remember to set all members to the same values and have exactly one member be set to act as the host. The chat experience will be improved if every user has a different user name but this does not affect the behavior of the MultiCommFramework as the user name is a chat application concept.
With this in mind you can set up a much more involved demonstration by using multiple smart devices and desktops. To setup a more involved demonstration, do the following:
- Launch MultiCommChat/MultiCommChat_DT on as many smart devices and desktops as you like. Remember to run only one instance of each per smart device/desktop. By the nature of network sockets, multiple programs cannot connect to the same port on a single computer.
- On each smart device instance except one, open Configuration/Options and uncheck the "Act as conversation Host" checkbox. Having multiple hosts will result in eratic behavior.
- For the best chat experience, open Conversation/Options and change the user names so that each instance has a different value.
- Choose Conversation/Join on each instance. You'll see the host acknowledge each client joining. Again the order that clients and the listener are started doesn't matter.
- Enter a message on one of the devices/desktops and tap/click Send. The message will appear on all of the other devices/desktops. Repeat as frequently as you like.
- Choose Conversation/Leave or exit each application instance. The chat programs automatically call Stop and therefore leave/terminate the conversation when exiting.
As previously mentioned, the MultiCommFramework is modeled after the concept of multicasting. The listener listens for clients to register interest in the conversation. From that point, when any application calls Send, the message is sent to the listener who then distributes the message to each client. All information is sent via UDP datagrams as connection-oriented protocols such as TCP do not work well if network connectivity is intermittent as is often the case with wireless networks.
The MultiCommFramework encapsulates all of the details of communication management allowing the application developer to focus on the application space. No knowledge of network communications is required. For those users who are network savvy and who may have special requirements, the classes are designed to be easily extended and modified.
A core goal of the design is to minimize the level of awareness of the underlying process on the application. As part of this, the concept of a particular application instance being the listener should be minimally invasive on the code. This was achieved by creating an inheritance relationship between MultiCommListener and MultiCommClient. If you look at the code in MultiCommChat, you'll see that there are only about 6 lines of code specific to being the listener. In fact once the listener is created; it is downcast to a MultiCommClient and is indiscernible from an instance of MultiCommClient.
Let's look at the architecture first by understanding the role of the client and then we'll look at the listener. Once we understand each, we'll take a look at the messages passed between them.
The client's primary role is to send and receive messages. Receiving messages is an asynchronous process and therefore run on a background thread. This thread is launched when the Start method is called. All of the details of receiving these incoming messages are encapsulated in the ReceiveMessagePump method. This method does little more then block on an incoming UDP socket waiting for a message. When the message is received it hands off to the RecieveDispatchMessage method for processing.
RecieveDispatchMessage is a virtual function with specific implementations for the client and the listener. In the case of the client, processing is implemented as a state machine. The client runs in three distinct states
- Waiting for an invitation: We're running but not yet in a conversation. The only thing the client will respond to at this point is an invitation to join a conversation.
- Waiting for invitation acknowledgement: We've received an invitation. We then asked to join the conversation and are waiting for the listener to acknowledge us. The only thing the client will respond to at this point is an acknowledgement. If that doesn't come, the client will role back to waiting for an invitation
- In a conversation: We are connected to a listener and sending/receiving messages. This is where the client will spend most of its time. We primarily are processing conversation messages but are also watching for a conversation termination message. If a termination message comes it means the listener is shutting down so we role back to waiting for an invitation.
The sending of messages is pretty simple. Once we're in a conversation, any call to the Send method results in a UDP datagram containing the passed data being sent to the listener. Because datagrams are not reliably delivered (although there rarely fail unless there are network connectivity problems), the client watches for the listener to acknowledge receipt of the message. If not acknowledged, the client will resend the message up to 3 times. The client is implemented to do this send as a blocking operation and may block for up to 1,500 milliseconds trying to perform the send.
By deriving from the MultiCommClient, MultiCommListener need only implement that behavior that is specific to it. It inherits all of the background receiving thread handling but provides its own custom message processing by overriding the implementation of RecieveDispatchMessage.
The RecieveDispatchMessage implementation is relatively simple as compared to the client. The only thing the listener need process is a clients acceptance of an invitation, request for an invitation, a conversation message and a request to leave the conversation. There is no need for a state machine.
The complexity of the listener comes from the need to distribute messages to its list of clients. To do this the listener creates an additional background thread that distributes messages to the clients. The details of distributing these messages are encapsulated in the DistributeMessagePump method. It basically monitors a queue for messages to distribute. It has the ability to send a message to each registered client, to a specific client or to send a broadcast. Currently only the listener sends messages to a specific client (this is used to acknowledge a message or respond to an invite request). Messages sent to the listener by clients are always distributed to all other clients.
All communication between the listener and the clients occurs through a set of 6 messages. The details of creating and decoding these messages are contained in the class MultiCommMessageManager and are identified by the enum OpCodes.
The following is the list of message types.
- OpCodes.Invite: Sent by the listener to invite clients to join the conversation. This message is sent via Broadcast when the SendInvitation method is called. It can also be sent to a specific client if that client broadcasts an OpCodes.InviteRequest message.
- OpCodes.InviteRequest: Sent by a client to request that an available listener invite it to join the conversation. This message is sent via Broadcast when the SendInviteRequest method is called.
- Opcodes.InviteAccept: Sent by the client to the listener in response to an Opcodes.Invite message. The listener then registers that client to receive conversation messages. Results in the OnJoin event being fired on the listener and that specific client.
- OpCodes.ConversationBody: Sent by the client when the Send method is called. The listener then sends this same message to each registered client. The MultiCommListener implementation of Send is optimized to queue the message for distribution directly. When the clients or listener receive this message, the OnReceive event fires.
- OpCodes.LeaveConversation: Sent by the client when the Stop method is called to request that it no longer receive conversation messages. The listener unregisters the client and will send it no further messages. The OnLeave event is fired on the listener and that specific client.
- OpCodes.TerminateConversation: Sent by the listener to each client when the Stop method is called to indicate that it is ending the conversation. The OnTerminate message is fired on the listener and each client.
The MultiCommFramework provides an easy to use framework for building network oriented smart device applications and is built to be tolerant to the loosely connected nature of wireless networks. Using the MultiCommFramework, smart device application developers can add network support to their applications without getting caught up in the hassles and delays of building wireless network friendly communications behavior.
I had a lot of fun building the MultiCommFramework I hope you have just as much fun using it. Even more importantly, I hope you find it as useful as I have.