Introduction to the UCMA API - Part 8 - Breaking the connection
Welcome to part eight of my UCMA introduction! This so far is my most ambitious blog endeavor to date and I hope what I have presented so far has been helpful. Please do send any feedback. I’d love to hear if this information has helped anyone or any requests for more information or particular problems you have had.
I also apologize for the rather slow pace so far (it does seem like it’s taking us awhile to send a message – doesn’t it?) but I have some exciting things planned for the future and very soon we will be able to do some interesting thing with our custom client and server.
Today we are going to handle what happens when one side of the dialog breaks the connection. We’ll divide this into two problems.
1) If we are the server, how can we tell if a client disconnected?
2) If we are the client, how can we tell if the server disconnected?
You’ve probably already noticed that the client and server do nothing when the other has disconnected. Let’s start with the server first. First, since our server should technically accept multiple clients, let’s build that in. We’ll do that by creating a new object that handles each client session.
In our server, add a new code file called IDKStudioSession.cs. We will differentiate our clients by a session number that increments itself (we don’t really need to do this but it helps for debugging reasons) and we will hold a SignalingSession object. Add the following fields to the new class.
private int _clientNumber;
private SignalingSession _session;
Our constructor is also very simple.
public IDKStudioSession(int clientNumber, SignalingSession session)
{
_clientNumber = clientNumber;
_session = session;
}
Now let’s add some events to the class. We need a way to propagate errors and messages back to our UI and we need to tell the server manager when a session has finished.
/// <summary>
/// Occurs when some progress occurs
/// </summary>
public event EventHandler<ProgressEventArgs> ProgressUpdated;
/// <summary>
/// Raise when an error has occured
/// </summary>
public event EventHandler<ProgressEventArgs> ErrorOccured;
/// <summary>
/// Occurs when the request has been completed
/// </summary>
public event EventHandler<CompletedEventArgs> SessionCompleted;
Let’s also create some helper methods in IDKStudioSession similar to the ones we already have.
/// <summary>
/// Records progress
/// </summary>
/// <param name="message">The message</param>
/// <param name="args">Arguments to the message</param>
private void RecordProgress(string message, params object[] args)
{
RecordProgress(String.Format(CultureInfo.CurrentCulture, message, args));
}
/// <summary>
/// Records progress
/// </summary>
/// <param name="message">The message</param>
private void RecordProgress(string message)
{
if (null != ProgressUpdated)
{
ProgressUpdated(this, new ProgressEventArgs(message));
}
}
/// <summary>
/// Raises an error
/// </summary>
/// <param name="message">The message of the error</param>
private void Error(string message)
{
if (null != ErrorOccured)
{
ErrorOccured(this, new ProgressEventArgs(message));
}
}
/// <summary>
/// Raises an error
/// </summary>
/// <param name="message">The message of the error</param>
/// <param name="args">Arguments to pass to the message</param>
private void Error(string message, params object[] args)
{
Error(string.Format(CultureInfo.CurrentCulture, message, args));
}
We’ll store our session in our server manager in a dictionary. We also need to keep a counter we can use when we receive a new session. Add the following fields to the server manager.
private Dictionary<int, IDKStudioSession> _sessions;
private int _currentSessionNumber = 0;
private object _syncObject = new object();
private bool _acceptingSessions = true;
We need the _syncObject due to threading issues that will appear later. We also added a Boolean flag that defaults to true indicating we are accepting new sessions. When the server needs to shut down, we will set this to false.
We’ll need to instantiate the dictionary somewhere. We could do this in the constructor but since everything happens from Start we might as well place it there. Change the code of Start() to the following.
_sessions = new Dictionary<int, IDKStudioSession>();
RegisterEndpoint();
Assuming we have received a session, we need a method to create an IDKStudioSession instance, place it in the dictionary, and hook up the event handlers. Add the following method to the server manager.
/// <summary>
/// Creates a new IDKStudioSession instance and runs it
/// </summary>
/// <param name="session">The signaling session we received</param>
private void CreateNewSession(SignalingSession session)
{
// Get a new unique Id for this client
int clientId = Interlocked.Increment(ref _currentSessionNumber);
// Create the IDKStudioSession instance
IDKStudioSession studioSession = new IDKStudioSession(clientId, session);
studioSession.ErrorOccured += new EventHandler<ProgressEventArgs>( studioSession _ErrorOccured);
studioSession.ProgressUpdated += new EventHandler<ProgressEventArgs>( studioSession _ProgressUpdated);
studioSession.SessionCompleted += new EventHandler<CompletedEventArgs>( studioSession _SessionCompleted);
// Save the session
lock (_syncObject)
{
_sessions.Add(studioSession.ClientId, studioSession);
}
// Start the session
studioSession.Start();
}
You will need to add a using statement for System.Threading for this to work, though we still have a bit of work to do before this compiles. We need to use the Interlocked.Increment function to ensure each session has its own unique number. We should also lock the dictionary to prevent multiple adds from occurring at the same time. Let’s go back to the IDKStudioSession class and add an empty Start method and a simple property for the client Id.
/// <summary>
/// Starts working with the session
/// </summary>
public void Start()
{
}
/// <summary>
/// The Id of the client for debugging purposes
/// </summary>
public int ClientId
{
get { return _clientNumber; }
set { _clientNumber = value; }
}
Now let’s work on two event handlers, one for session progress and the second for session errors. In the case of an error, we don’t want to call Error in the UI because that will signal the end of the server. Any client error can be recovered from by killing the client. We won’t do that here – our client session should kill itself if it deems the error unrecoverable. So when the Error event is raised we will just log the appropriate message. Note that we log the client Id so in the UI you can see which session the message relates to.
/// <summary>
/// Occurs when an individual session reports progress
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void studioSession_ProgressUpdated(object sender, ProgressEventArgs e)
{
IDKStudioSession studioSession = sender as IDKStudioSession;
RecordProgress("[{0}] - {1}", studioSession.ClientId, e.Message);
}
/// <summary>
/// Occurs when an individual session reports an error
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void studioSession_ErrorOccured(object sender, ProgressEventArgs e)
{
IDKStudioSession studioSession = sender as IDKStudioSession;
RecordProgress("[{0} ERROR - {1}", studioSession.ClientId, e.Message);
}
We can now look at the SessionCompleted event. Basically we will receive this event when the connection is no longer needed. We’re still not sure how the IDKStudioSession class will determine this, but this is how it will inform our manager of the fact. For now, we will log this fact and remove the session from the dictionary.
/// <summary>
/// Occurs when an individual session as completed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void studioSession_SessionCompleted(object sender, CompletedEventArgs e)
{
IDKStudioSession studioSession = sender as IDKStudioSession;
RecordProgress("[{0}] - Session ended({1}) - {2}",
studioSession.ClientId,
e.Status,
e.Message);
lock (_syncObject)
{
_sessions.Remove(studioSession.ClientId);
}
}
When a session has completed, we simply display a message and remove it from our dictionary. We have to synchronize our access to the dictionary when removing the session with the lock.
Before we can look into how the IDKStudioSession determines whether the session has disconnected, we must first cover the scenario where the user clicks ‘stop’ to shut down the server. In this case we must close all of our sessions. To do this we will do the following.
1) Tell each IDKStudioSession to shut down
2) As each session shuts down, it raises the SessionCompleted event
3) When we receive this event, we remove it from the dictionary
4) When the dictionary is empty, we call another method that continues the shutdown process
First, let’s add a way to terminate each session. In IDKStudioSession add a new Terminate method.
/// <summary>
/// Terminates the session
/// </summary>
public void Terminate()
{
_session.BeginTerminate(new AsyncCallback(TerminateCallback), _session);
}
This is simple enough, now for the callback.
/// <summary>
/// Callback for the terminate method
/// </summary>
/// <param name="result"></param>
private void TerminateCallback(IAsyncResult result)
{
SignalingSession session = result.AsyncState as SignalingSession;
try
{
session.EndTerminate(result);
if (null != SessionCompleted)
{
SessionCompleted(this, new CompletedEventArgs(CompletionStatus.Success, "Session terminated"));
}
}
catch (RealTimeException ex)
{
Error("Unable to terminate the session: {0}", ex.ToString());
}
}
Here, as you can see, once we have terminated the session we raise the SessionCompleted event which notifies our code that the session is finished. Now we need to revisit the Terminate method in the manager which is gradually becoming more complicated. What we need to do now is the following.
1) Set _acceptingSessions to false which will prevent any new sessions from being accepted
2) Call Terminate in each of our sessions
3) As each session calls us back with the SessionCompleted event, check to see if there are any more sessions in the dictionary. If there are no more sessions and _acceptingSessions is false, we call unregister.
4) In the unregister callback, call BeginTerminate to terminate the endpoint
Here is the new Terminate method for the server manager.
/// <summary>
/// Terminates the current session and endpoint
/// </summary>
public void Terminate()
{
RecordProgress("Terminating the manager");
lock (_syncObject)
{
_acceptingSessions = false;
foreach (IDKStudioSession session in _sessions.Values)
{
session.Terminate();
}
}
}
We must also add a check for _acceptingSessions when deciding whether to accept the session. The following is our new SessionReceived event handler.
void SessionReceived(object sender, SessionReceivedEventArgs e)
{
RecordProgress("An invite was received.");
// Accept the invite
lock (__syncObject)
{
if (false == _acceptingSessions)
{
RecordProgress("Terminating session because we are not accepting any more");
e.Session.BeginTerminate(new AsyncCallback(NonAcceptedSessionTerminateCallback), e.Session);
}
else
{
e.Session.BeginParticipate(new AsyncCallback(ParticipateCallback), e.Session);
}
}
}
If we are not accepting sessions, we just terminate it. We now need to implement the terminate callback. This is very similar to previous callbacks.
/// <summary>
/// Called when we terminate a session that was never accepted
/// </summary>
/// <param name="ar"></param>
private void NonAcceptedSessionTerminateCallback(IAsyncResult ar)
{
SignalingSession session = ar.AsyncState as SignalingSession;
try
{
session.EndTerminate(ar);
RecordProgress("Non-accepted session terminated.");
}
catch (RealTimeException ex)
{
RecordProgress("Unable to terminate non-accepted session: {0}", ex.ToString());
}
}
We now need to add code to call unregister if we are shutting down. The following is our complete SessionCompleted event handler.
void studioSession_SessionCompleted(object sender, CompletedEventArgs e)
{
IDKStudioSession studioSession = sender as IDKStudioSession;
RecordProgress("[{0}] - Session ended({1} - {2}",
studioSession.ClientId,
e.Status,
e.Message);
lock (_syncObject)
{
_sessions.Remove(studioSession.ClientId);
if (_sessions.Count == 0 && _acceptingSessions == false)
{
// Terminate the endpoint
_endPoint.BeginUnregister(new AsyncCallback(UnregisterCallback), _endPoint);
}
}
}
The very last thing you need to do to get the multiple client scenario to work is add a new call to CreateSession in the server manager if successful. The following code should be in the try block.
//This response tells us if the session creation succeeded.
response = session.EndParticipate(ar);
RecordProgress("The invite was accepted successfully.");
CreateNewSession(session);
Today was a bit of a strange day in that we did not accomplish what I originally set out to explain. Our server and client are still not able to determine when either one has closed the session but that will be the topic for tomorrow’s post. We do now have, though, a server that is capable of connecting to multiple clients.
Comments
- Anonymous
June 14, 2009
PingBack from http://cutebirdbaths.info/story.php?id=4696