After I posted my earlier entry on debugging WCF apps, I thought it would be a good idea to explain the different WCF runtime blocks . This way users can see how each block/class fits in the bigger picture and how the runtime processes new channels/messages. When Open() is called on a ServiceHost, the runtime is built from the ServiceDescription and the list of ServiceEndpoints specified. Once the Open() step completes, the runtime is immutable and any attempt to modify any components results in an exception.
Let’s see how the runtime looks once all listeners have been started.
Based on your base addresses and EndpointAddress, the service host contains a list of listen URI’s and each URI will have its own listener. There is a ChannelDispatcher associated with each ListenUri and each ChannelDispatcher can contain 1 or more EndpointDispatcher object. Most often a ChannelDispatcher will contain exactly one EndpointDispatcher but for the case where the user has hosted multiple endpoints all using one listen URI(refer this post). In that case, there will be exactly one ChannelDispatcher for that Uri but it will contain all the EndpointDispatchers for all the ServiceEndpoints. Basically, each ServiceEndpoint added to the host will have one EndpointDispatcher associated with it. Each EndpointDispatcher contains the DispatchRuntime class that contains extension points InstanceContextProvider, InstanceContextInitializers etc.
Each ChannelDispatcher contains one ListenerHandler which, as its name suggests, handles the underlying IListener. This class is the one which determines when to accept a new channel. When the ListenerHandler accepts a new Channel, it will wrap it with a ChannelHandler object. ChannelHandler handles messages off one particular channel in the same way ListenerHandler handles connection requests from one listener.
Let’s see a sample breakdown of how a channel is accepted and messages are read till the channel closes. Note that I have tried to simplify this as explaining the actual process would be very verbose.
ServiceHost is opened and all Listeners are ready to accept.
ListenerHandler object opens and issues a pending Accept() call
When a client connects to the listener a callback is fired on the ListenerHandler notifying availability of a channel.
The ListenerHandler creates a ChannelHandler object and associated the channel with that handler.
The ChannelHandler object is registered. Once registered, the ChannelHandler will try to read messages from that channel.In other words, it issues a pending Receive on that channel.
When the client sends a message, ChannelHandler is notified of that message via a callback.
The handler then determines the EndpointDispatcher that the message is addressed to.
Then using the DispatchRuntime of that EndpointDispatcher it determines the Operation that the runtime should invoke and schedules the invocation.
It keeps reading Messages from the channel till null is received (Denoting a client initiated Close())
So we refer to the ListenerHandler’s way of reading channels as ChannelPump and similarly the ChannelHandler’s behavior is referred as MessagePump. It’s the responsibility of the individual handlers to ensure that their respective pumps are never stalled. A stalled pump will mean either no new connections are accepted or no new message will be read off a channel. The ListenerHandler uses the ServiceThrottleBehavior.MaxConcurrenctConnections throttle to determine when to pause and restart the ChannelPump and the ChannelHandler will use the ServiceThrottleBehavior.MaxConcurrenctCalls throttle to determine when to pause and restart the MessagePump.