This is one in a series of articles about Time Tortoise, a Universal Windows Platform app for planning and tracking your work schedule. For more on the development of this app and the ideas behind it, see my Time Tortoise category page.
To communicate with its companion app, Time Tortoise needs to use sockets. In recent weeks, I have been experimenting with socket communication between a UWP client app and a server running in a console app. This architecture works when everything is set up and running as intended. But it can be tricky to make sockets robust. So this week I’m looking into adding a layer that makes sockets easier to use and more resistant to unexpected failures.
Socket Scenarios
Basic communication
Time Tortoise: A Companion App summarizes how to set up a UWP client and a non-UWP server so they can communicate over sockets. Two key client method calls used in that arrangement are:
Socket.ConnectAsync
: Given a server name (or IP address) and port, connect the client and server.StreamWriter.FlushAsync
: This method is used afterWriteLineAsync
to exchange data between the client and server.
If all of the steps happen in order and both the client and server continue running, everything works well. If not, various exception conditions are possible. Here are some examples:
Server isn’t listening
If there’s no server listening on the specified address and port, ConnectAsync
throws a COMException
with the message “No connection could be made because the target machine actively refused it.”
Example: This happens if you try to connect the client without first starting the server.
Client and server are not connected
Attempting to write data to a disconnected socket using StreamWriter.FlushAsync
results in an InvalidOperationException
with the message “A method was called at an unexpected time.”
Example: This happens if you try to send data from the client without first connecting to the server.
Client tries to connect using a socket that is already connected
If ConnectAsync
is called on a socket that’s already connected, it throws an InvalidOperationException
with the message “A method was called at an unexpected time.”
Example: This happens if you start the server, connect the client, and then try to connect the client again.
Client and server connect, but then server shuts down
If the client and server were connected through a socket, but the server shuts down unexpectedly, writing to the socket using StreamWriter.FlushAsync
results in a COMException
with the message “An existing connection was forcibly closed by the remote host.”
Example: This happens if you start the server, connect the client, terminate the server, and then try to send data from the client.
SignalR
In addition to the examples described above, many other combinations of events can happen that cause problems for the socket connection. It’s possible to defend against these invalid conditions, but as with many things in software development, someone else has already done that work. Inspired by this Stack Overflow answer (“If at all possible, use SignalR instead of raw sockets. They do all the hard stuff for you.”), I decided to try SignalR.
SignalR was created to allow ASP.NET developers to write web applications in which the server contacts the client with new information. This supplements the usual web development pattern in which the client contacts the server to request information.
Although it was designed for ASP.NET, SignalR doesn’t require a web server. It works fine with other kinds of apps. In particular, I found an example of a simple chat app in which the client uses UWP and the server runs on the same machine. This is exactly what we need for Time Tortoise and Time Tortoise Companion.
SignalR Chat
The SignalR chat example works as follows.
Server
In this example, the server is a console app. It first does the following:
- Start a
WebApp
(fromMicrosoft.Owin.Hostring
) on a port on localhost. OWIN is the Open Web Interface for .NET. This is what allows this console app to use SignalR, an ASP.NET technology, without running in an IIS web server context. - Call
MapSignalR
, which makes a SignalR endpoint available at/signalr
.
Client
Once the server is running, the client can connect to it. In this sample app, the user clicks the Join button to join a chat group. This causes the client to do the following:
- If the client and server are not yet connected, call
HubConnection.Start
to connect. - Invoke the
JoinGroup
method on the server usingIHubProxy.Invoke
. SignalR allows the client to call methods on the server using this pattern.
Server
The JoinGroup
method adds the client to the new group. Groups are used by SignalR to determine which clients to send messages to. For Time Tortoise, we’ll probably just need one group, since we’ll only have one client.
Client
Once joined to a group, the client can send messages to the chat session. As with joining a group, this involves invoking a server method. In this case, the method is called SendToGroup
.
Server
In the SendToGroup
method, the server calls ReceiveMessage
on the current group.
Client
The client has previously set up a handler for ReceiveMessage
. Therefore, when the message arrives from the server, a client method that is also named ReceiveMessage
is called. This method prints the chat message to the client window.
Time Tortoise and Time Tortoise Companion
A chat client that can exchange messages with a chat server is exactly we need for Time Tortoise. However, rather than a user typing a chat message, in Time Tortoise the messages will all be generated automatically by the client and server programs. For example, when the server (Time Tortoise Companion) has new data about user idle time, it can send it to the client (Time Tortoise) by invoking a method on the current SignalR group. This will cause a method to be called in Time Tortoise to receive the data and take appropriate action in the Time Tortoise app.