I received a question regarding this post on WCF and what my handlers look like when a client disconnects (either because of a fault or the client connection is closed). It's fairly simple. Here's the code used to hook up the events:
IClientCallback remoteMachine = OperationContext.Current.GetCallbackChannel<IClientCallback>();
OperationContext.Current.Channel.Faulted += new EventHandler(ClientFaulted);
OperationContext.Current.Channel.Closed += new EventHandler(ClientClosed);
As a side note, I haven't quite gotten in the habit of using the new/shortened syntax for hooking up delegates. The code above can actually now be written as:
IClientCallback remoteMachine = OperationContext.Current.GetCallbackChannel<IClientCallback>();
OperationContext.Current.Channel.Faulted += ClientFaulted;
OperationContext.Current.Channel.Closed += ClientClosed;
At any rate, the code in both handlers is actually the same, so I'll just show ClientClosed:
/// <summary>
/// Called whenever a client machine's connection is closed.
/// Automatically removes them from our internal list of clients.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ClientClosed(object sender, EventArgs e)
{
IClientCallback remoteMachine = sender as IClientCallback;
this.RemoveClientMachine(remoteMachine);
}
All it does is cast the sender to the IClientCallback interface and call another method which actually removes it from my internal list. Here's what that code is doing (actually, I send out another notification in the real code to any other clients to let them know something has changed). It just locks the list then uses a lambda to find the client in the list, and if it's found, it's removed.
private void RemoveClientMachine(IClientCallback remoteMachine)
{
if (remoteMachine != null)
{
RegisteredClient client;
// Unregister them automatically
lock (m_callbackList)
{
client = m_callbackList.Find(c => c.CallBack == remoteMachine);
if (client != null)
m_callbackList.Remove(client);
}
One interesting failure scenario I found occurred when you had a large number of clients connected and something like your main network line goes down. In some cases I wouldn't receive a notification for every client to remove them from a list (I'm guessing it was firing so many events some of them were being lost). At any rate, the easiest way for me to address this was to include a watchdog timer which would periodically sweep through the connections and attempt to determine if they were still valid. Here's what that looks like:
public void CheckCallbackChannels()
{
RegisteredClient[] clientList = new RegisteredClient[0];
lock (m_callbackList)
{
clientList = new RegisteredClient[m_callbackList.Count];
m_callbackList.CopyTo(clientList);
foreach (RegisteredClient registeredClient in clientList)
{
ICommunicationObject callbackChannel = registeredClient.CallBack as ICommunicationObject;
if (callbackChannel.State == CommunicationState.Closed || callbackChannel.State == CommunicationState.Faulted)
{
this.RemoveClientMachine(registeredClient.CallBack);
}
}
}
}
I've been working on an application which relies heavily on WCF to communicate. I have a very simple interface (contract) that the server supports to connect and disconnect: it exposes the methods Register() and Unregister(). When the client starts up and connects, it calls the Register() method, passing in some identification info. I save this information in a List<T> so that it can be used by the server to provide callbacks. When the client disconnects, it calls Unregister() which then removes the client from the collection.
That works well enough, right up until a client abruptly disconnects (ex. a connection error, network line goes down, etc.) Then suddenly I end up with a reference to a disconnected client on the server. If I attempt to use this connection to send a message back to the client an exception is thrown (which still works OK). It would be nice to know immediately as soon as a client disconnects.
If you are using TCP (full duplex) as the communication channel, you can do this pretty easily - note: this doesn't work for some of the other communication modes, like HTTP duplex. For those, you might have to rely on the various timeout settings in the .config file.
Here's what that ends up looking like (I happen to be taking advantage of LINQ here, but you can easily adjust that code if you're using an older vesion of the framework):
203 private void IServer.Register(string systemName)
204 {
205 IClientCallback remoteMachine = OperationContext.Current.GetCallbackChannel<IClientCallback>();
206 // Hook up events to let us know if the client disconnects w/o telling us.
207 OperationContext.Current.Channel.Faulted += new EventHandler(ClientFaulted);
208 OperationContext.Current.Channel.Closed += new EventHandler(ClientClosed);
209
210 // Get object reference to figure out the IP address of the connected client
211 MessageProperties prop = OperationContext.Current.IncomingMessageProperties;
212 RemoteEndpointMessageProperty endpoint = prop[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
213
214 // (threading/locking code removed)
215
216 client = m_callbackList.Find(c => c.SystemName == systemName);
217 if (client == null)
218 {
219 // It's not already on our list, add it
220 m_callbackList.Add(new RegisteredClient(systemName, endpoint.Address, remoteMachine));
221 }
222 }