I am in the process of writing a WCF Duplex service for a chat application with a WPF client. The service code is below
IChatCallback
public interface IChatCallback
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true)]
void Receive(Person sender, string message);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Person sender, string message);
[OperationContract(IsOneWay = true)]
void UserEnter(Person person);
[OperationContract(IsOneWay = true)]
void UserLeave(Person person);
#endregion
}
IChatService
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]
public interface IChatService
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Say(string message);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Whisper(string to, string message);
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
Person[] Join(Person person);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
void Leave();
#endregion
}
ChatService
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChatService
{
#region Static Fields
private static object _syncObj = new object();
private static Dictionary<Person, ChatEventHandler> _chatters = new Dictionary<Person, ChatEventHandler>();
#endregion
#region Fields
private IChatCallback _callback = null;
private ChatEventHandler _myEventHandler;
private Person _person;
#endregion
#region Delegates
public delegate void ChatEventHandler(object sender, ChatEventArgs e);
#endregion
#region Public Events
public static event ChatEventHandler ChatEvent;
#endregion
#region Public Methods and Operators
public void Say(string message)
{
ChatEventArgs e = new ChatEventArgs(MessageType.Receive, this._person, message);
this.BroadcastMessage(e);
}
public void Whisper(string to, string message)
{
ChatEventArgs e = new ChatEventArgs(MessageType.ReceiveWhisper, this._person, message);
try
{
ChatEventHandler chatterTo;
lock (_syncObj)
{
chatterTo = this.GetPersonHandler(to);
if (chatterTo == null)
{
throw new KeyNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"The person with name [{0}] could not be found",
to));
}
}
chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
}
catch (KeyNotFoundException)
{
}
}
public Person[] Join(Person person)
{
bool userAdded = false;
this._myEventHandler = new ChatEventHandler(this.MyEventHandler);
lock (_syncObj)
{
if (!this.CheckIfPersonExists(person.Name) && person != null)
{
this._person = person;
_chatters.Add(person, this.MyEventHandler);
userAdded = true;
}
}
if (userAdded)
{
this._callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();
ChatEventArgs e = new ChatEventArgs(MessageType.UserEnter, this._person);
this.BroadcastMessage(e);
ChatEvent += this._myEventHandler;
Person[] list = new Person[_chatters.Count];
lock (_syncObj)
{
_chatters.Keys.CopyTo(list, 0);
}
return list;
}
else
{
return null;
}
}
public void Leave()
{
if (this._person == null)
{
return;
}
ChatEventHandler chatterToRemove = this.GetPersonHandler(this._person.Name);
lock (_syncObj)
{
_chatters.Remove(this._person);
}
ChatEvent -= chatterToRemove;
ChatEventArgs e = new ChatEventArgs(MessageType.UserLeave, this._person);
this.BroadcastMessage(e);
}
#endregion
private void MyEventHandler(object sender, ChatEventArgs e)
{
try
{
switch (e.MessageType)
{
case MessageType.Receive:
this._callback.Receive(e.Person, e.Message);
break;
case MessageType.ReceiveWhisper:
this._callback.ReceiveWhisper(e.Person, e.Message);
break;
case MessageType.UserEnter:
this._callback.UserEnter(e.Person);
break;
case MessageType.UserLeave:
this._callback.UserLeave(e.Person);
break;
}
}
catch
{
this.Leave();
}
}
private void BroadcastMessage(ChatEventArgs e)
{
ChatEventHandler temp = ChatEvent;
if (temp != null)
{
foreach (ChatEventHandler handler in temp.GetInvocationList())
{
handler.BeginInvoke(this, e, new AsyncCallback(this.EndAsync), null);
}
}
}
private bool CheckIfPersonExists(string name)
{
foreach (Person p in _chatters.Keys)
{
if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private void EndAsync(IAsyncResult ar)
{
ChatEventHandler d = null;
try
{
AsyncResult asres = (AsyncResult)ar;
d = (ChatEventHandler)asres.AsyncDelegate;
d.EndInvoke(ar);
}
catch
{
ChatEvent -= d;
}
}
private ChatEventHandler GetPersonHandler(string name)
{
foreach (Person p in _chatters.Keys)
{
if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
ChatEventHandler chatTo = null;
_chatters.TryGetValue(p, out chatTo);
return chatTo;
}
}
return null;
}
}
This is hosted in a console application with an endpoint of net.tcp://localhost:33333/chatservice using the netTcpBinding with the following binding configuration
<system.serviceModel>
<services>
<service name="Cleo.Services.Chat.ChatService" behaviorConfiguration="CleoChatBehavior">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:33333/chatservice"/>
</baseAddresses>
</host>
<endpoint address="" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="Cleo.Services.Chat.IChatService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="CleoChatBehavior">
<serviceThrottling maxConcurrentSessions="10000"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<netTcpBinding>
<binding name="DuplexBinding" maxBufferSize="67108864" maxReceivedMessageSize="67108864" maxBufferPoolSize="67108864" transferMode="Buffered" closeTimeout="00:00:10" openTimeout="00:00:10" receiveTimeout="00:20:00" sendTimeout="00:01:00" maxConnections="100">
<reliableSession enabled="true" inactivityTimeout="00:20:00" />
<security mode="None" />
<readerQuotas maxArrayLength="67108864" maxBytesPerRead="67108864" maxStringContentLength="67108864" />
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
In my WPF client I have implemented a proxy to the service using svcutil which is below:
IChatServiceCallback
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public interface IChatServiceCallback
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/Receive")]
void Receive(Person sender, string message);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/Receive")]
IAsyncResult BeginReceive(Person sender, string message, AsyncCallback callback, object asyncState);
void EndReceive(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/ReceiveWhisper")]
void ReceiveWhisper(Person sender, string message);
[OperationContract(IsOneWay = true, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/ReceiveWhisper")]
IAsyncResult BeginReceiveWhisper(Person sender, string message, AsyncCallback callback, object asyncState);
void EndReceiveWhisper(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/UserEnter")]
void UserEnter(Person person);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/UserEnter")]
IAsyncResult BeginUserEnter(Person person, AsyncCallback callback, object asyncState);
void EndUserEnter(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/UserLeave")]
void UserLeave(Person person);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/UserLeave")]
IAsyncResult BeginUserLeave(Person person, AsyncCallback callback, object asyncState);
void EndUserLeave(IAsyncResult result);
#endregion
}
IChatService
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
[ServiceContract(ConfigurationName = "IChatService", CallbackContract = typeof(IChatServiceCallback),
SessionMode = SessionMode.Required)]
public interface IChatService
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChatService/Say")]
void Say(string message);
[OperationContract(IsOneWay = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Say")]
IAsyncResult BeginSay(string message, AsyncCallback callback, object asyncState);
void EndSay(IAsyncResult result);
[OperationContract(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChatService/Whisper")]
void Whisper(string to, string message);
[OperationContract(IsOneWay = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Whisper")]
IAsyncResult BeginWhisper(string to, string message, AsyncCallback callback, object asyncState);
void EndWhisper(IAsyncResult result);
[OperationContract(Action = "http://tempuri.org/IChatService/Join",
ReplyAction = "http://tempuri.org/IChatService/JoinResponse")]
Person[] Join(Person person);
[OperationContract(AsyncPattern = true, Action = "http://tempuri.org/IChatService/Join",
ReplyAction = "http://tempuri.org/IChatService/JoinResponse")]
IAsyncResult BeginJoin(Person person, AsyncCallback callback, object asyncState);
Person[] EndJoin(IAsyncResult result);
[OperationContract(IsOneWay = true, IsTerminating = true, IsInitiating = false,
Action = "http://tempuri.org/IChatService/Leave")]
void Leave();
[OperationContract(IsOneWay = true, IsTerminating = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Leave")]
IAsyncResult BeginLeave(AsyncCallback callback, object asyncState);
void EndLeave(IAsyncResult result);
#endregion
}
IChatServiceChannel
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public interface IChatServiceChannel : IChatService, IClientChannel
{
}
and ChatProxy
[DebuggerStepThrough]
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public class ChatProxy : DuplexClientBase<IChatService>, IChatService
{
#region Constructors and Destructors
public ChatProxy(InstanceContext callbackInstance)
: base(callbackInstance)
{
}
public ChatProxy(InstanceContext callbackInstance, string endpointConfigurationName)
: base(callbackInstance, endpointConfigurationName)
{
}
public ChatProxy(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress)
: base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}
public ChatProxy(
InstanceContext callbackInstance,
string endpointConfigurationName,
EndpointAddress remoteAddress)
: base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}
public ChatProxy(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress)
: base(callbackInstance, binding, remoteAddress)
{
}
#endregion
#region Public Methods and Operators
public void Say(string message)
{
this.Channel.Say(message);
}
public IAsyncResult BeginSay(string message, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginSay(message, callback, asyncState);
}
public void EndSay(IAsyncResult result)
{
this.Channel.EndSay(result);
}
public void Whisper(string to, string message)
{
this.Channel.Whisper(to, message);
}
public IAsyncResult BeginWhisper(string to, string message, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginWhisper(to, message, callback, asyncState);
}
public void EndWhisper(IAsyncResult result)
{
this.Channel.EndWhisper(result);
}
public Person[] Join(Person person)
{
return this.Channel.Join(person);
}
public IAsyncResult BeginJoin(Person person, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginJoin(person, callback, asyncState);
}
public Person[] EndJoin(IAsyncResult result)
{
return this.Channel.EndJoin(result);
}
public void Leave()
{
this.Channel.Leave();
}
public IAsyncResult BeginLeave(AsyncCallback callback, object asyncState)
{
return this.Channel.BeginLeave(callback, asyncState);
}
public void EndLeave(IAsyncResult result)
{
this.Channel.EndLeave(result);
}
#endregion
}
With the client configuration in the main application:
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="CleoDefaultBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="33554432" maxReceivedMessageSize="4194304" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="4194304" maxArrayLength="32768" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</wsHttpBinding>
<netTcpBinding>
<binding name="DuplexBinding" sendTimeout="00:00:30">
<reliableSession enabled="true"/>
<security mode="None"/>
</binding>
</netTcpBinding>
</bindings>
<client>
<!-- Cleo Chat Client -->
<endpoint name="CleoChatWcfServiceClient" address="net.tcp://localhost:33333/chatservice" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="IChatService"/>
<endpoint address="net.tcp://localhost:51638/services/chat/wcf" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="CleoChatClient.ICleoChatWcfService" name="chatWcfService" />
</client>
</system.serviceModel>
Ok, all good except for some reason i am getting an error when running the following code to connect to the service, the code is:
public class ProxySingleton : IChatServiceCallback
{
...
public void Connect(Person p)
{
var site = new InstanceContext(this);
this._proxy = new ChatProxy(site);
var iar = this._proxy.BeginJoin(p, this.OnEndJoin, null);
}
private void OnEndJoin(IAsyncResult ar)
{
try
{
var list = this._proxy.EndJoin(ar); --> Errors here!!
this.HandleEndJoin(list);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
...
}
The error i am getting is:
The remote endpoint requested an address for acknowledgements that is not the same as the address for application messages. The channel could not be opened because this is not supported. Ensure the endpoint address used to create the channel is identical to the one the remote endpoint was set up with.
My question (and sorry for the very long post but I am completely stuck on this) is simply whether anyone else has come across this and could point me towards an answer please?
EDIT: I have updated to include the full serviceModel sections from the server and client and also updated the ProxySingleton to show that it does implement the callback interface
Here's a fully functional setup for your ChatService
:
Host:
class ProgramHost
{
static void Main(string[] args)
{
try
{
ServiceHost host = new ServiceHost(typeof(ChatLib.ChatService));
host.Open();
Console.WriteLine(string.Format("WCF {0} host is running...", host.Description.ServiceType));
Console.WriteLine("Endpoints:");
foreach (ServiceEndpoint se in host.Description.Endpoints)
{
Console.WriteLine("***********************************************");
Console.WriteLine(string.Format("Address = {0}", se.Address));
Console.WriteLine(string.Format("Binding = {0}", se.Binding));
Console.WriteLine(string.Format("Contract = {0}", se.Contract.Name));
}
Console.WriteLine(string.Empty);
Console.WriteLine("Press <ENTER> to terminate.");
Console.ReadLine();
host.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
Client:
class ProgramClient
{
static void Main(string[] args)
{
try
{
if (args.Length != 1)
Console.WriteLine("usage: clientconsole username");
else
{
Person user = new Person(args[0]);
IChatServiceCallback callback = new SimpleChatCallback();
InstanceContext instanceContext = new InstanceContext(callback);
ChatServiceClient serviceProxy = new ChatServiceClient(instanceContext);
Console.WriteLine("Endpoint:");
Console.WriteLine("***********************************************");
Console.WriteLine(string.Format("Address = {0}", serviceProxy.Endpoint.Address));
Console.WriteLine(string.Format("Binding = {0}", serviceProxy.Endpoint.Binding));
Console.WriteLine(string.Format("Contract = {0}", serviceProxy.Endpoint.Contract.Name));
Person[] people = serviceProxy.Join(user);
Console.WriteLine("***********************************************");
Console.WriteLine("Connected !");
Console.WriteLine("Online users:");
foreach (Person p in people)
Console.WriteLine(p.Name);
string msg;
while ((msg = Console.ReadLine()) != "exit")
serviceProxy.Say(msg);
serviceProxy.Leave();
if (serviceProxy.State != CommunicationState.Faulted)
serviceProxy.Close();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Client callback:
public class SimpleChatCallback : IChatServiceCallback
{
public void Receive(Person sender, string message)
{
Console.WriteLine("{0}: {1}", sender.Name, message);
}
public void ReceiveWhisper(Person sender, string message)
{
Console.WriteLine("{0}: {1}", sender.Name, message);
}
public void UserEnter(Person person)
{
Console.WriteLine("{0} has entered", person.Name);
}
public void UserLeave(Person person)
{
Console.WriteLine("{0} has left", person.Name);
}
}
Host config:
<system.serviceModel>
<services>
<service behaviorConfiguration="mexBehavior" name="ChatLib.ChatService">
<clear />
<endpoint address="ChatService.svc" binding="netTcpBinding" bindingConfiguration=""
name="netTcpEndpoint" bindingName="NonSecureTcpBinding" contract="Common.IChatService" />
<endpoint binding="mexHttpBinding" bindingConfiguration="mexHttpBinding"
name="mexHttpEndpoint" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:33334/chatservice" />
<add baseAddress="net.tcp://localhost:33333/chatservice" />
</baseAddresses>
<timeouts openTimeout="00:10:00" />
</host>
</service>
</services>
<bindings>
<netTcpBinding>
<binding name="NonSecureTcpBinding">
<security mode="None">
<transport clientCredentialType="None" protectionLevel="None" />
<message clientCredentialType="None" />
</security>
</binding>
</netTcpBinding>
<mexHttpBinding>
<binding name="mexHttpBinding" />
</mexHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="mexBehavior">
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<protocolMapping>
<remove scheme="net.tcp" />
<add scheme="net.tcp" binding="netTcpBinding" bindingConfiguration="NonSecureTcpBinding" />
<add scheme="https" binding="basicHttpsBinding" />
</protocolMapping>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
Client config:
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="netTcpEndpoint" />
</netTcpBinding>
</bindings>
<client>
<endpoint address="net.tcp://localhost:33333/chatservice/ChatService.svc"
binding="netTcpBinding" bindingConfiguration="netTcpEndpoint"
contract="ServiceReference1.IChatService" name="netTcpEndpoint">
<identity>
<userPrincipalName value="ComputerName\UserName" />
</identity>
</endpoint>
</client>
</system.serviceModel>
Host output:
Client 1 output:
Client 2 output:
Notes:
ServiceReference1
is the default namespace assigned by Visual Studio to the generated proxy client ChatServiceClient
.
ChatLib
is the locally assigned namespace to your ChatService
implementation.