问题
最近的Silverlight开发中,由于部分需求对实时性和数据量下载速度有要求,部分WCF服务配置成了netTcpBinding,这种方式跟普通的service.svc寄宿IIS不同的是,Silverlight需要的策略文件需要放置在本机IIS的根下,也就是wwwroot文件夹下,以满足Silverlight在以TCP协议调用本机WCF服务时请求策略文件。(注:Silverlight通过TCP协议调用WCF服务时,会以http方式请求主机的一个策略文件,地址是http://localhost/clientaccesspolicy.xml)
这其实是个不太好的选择,程序运行的所需的环境被分成了两部分,同事的机器上并未安装IIS,为了大家开发简便,不用在额外安装IIS,也为了让程序更加独立,我就想能不能写代码监控80端口模拟IIS向Silverlight输出这个策略文件。
解决方法
有了这个想法之后,首先想到的是通过Socket进行监听,因为此前在MSDN上看到过这种方式,但很无奈,将代码转移过来之后,并未成功。相信做过Silverlight在Socket方面应用的朋友对下面这个PolicyServer类很熟悉吧。
using System; using System.IO; using System.Net; using System.Net.Sockets; namespace PolicyServer{ // Encapsulate and manage state for a single connection from a client class PolicyConnection{ private Socket m_connection; // buffer to receive the request from the client private byte [] m_buffer; private int m_received; // the policy to return to the client private byte [] m_policy; // the request that we're expecting from the client private static string s_policyRequestString = " <policy-file-request/> " ; public PolicyConnection(Socket client, byte [] policy) { m_connection = client; m_policy = policy; m_buffer = new byte [s_policyRequestString.Length]; m_received = 0 ; try { // receive the request from the client m_connection.BeginReceive(m_buffer, 0 , s_policyRequestString.Length, SocketFlags.None, new AsyncCallback(OnReceive), null ); } catch (SocketException) { m_connection.Close(); } } // Called when we receive data from the client private void OnReceive(IAsyncResult res) { try { m_received += m_connection.EndReceive(res); // if we haven't gotten enough for a full request yet, receive again if (m_received < s_policyRequestString.Length) { m_connection.BeginReceive(m_buffer, m_received, s_policyRequestString.Length - m_received, SocketFlags.None, new AsyncCallback(OnReceive), null ); return ; } // make sure the request is valid string request = System.Text.Encoding.UTF8.GetString(m_buffer, 0 , m_received); if (StringComparer.InvariantCultureIgnoreCase.Compare(request, s_policyRequestString) != 0 ) { m_connection.Close(); return ; } // send the policy m_connection.BeginSend(m_policy, 0 , m_policy.Length, SocketFlags.None, new AsyncCallback(OnSend), null ); } catch (SocketException) { m_connection.Close(); } } // called after sending the policy to the client; close the connection. public void OnSend(IAsyncResult res) { try { m_connection.EndSend(res); } finally { m_connection.Close(); } }} // Listens for connections on port 943 and dispatches requests to a PolicyConnection class PolicyServer{ private Socket m_listener; private byte [] m_policy; // pass in the path of an XML file containing the socket policy public PolicyServer( string policyFile) { // Load the policy file FileStream policyStream = new FileStream(policyFile, FileMode.Open); m_policy = new byte [policyStream.Length]; policyStream.Read(m_policy, 0 , m_policy.Length); policyStream.Close(); m_listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); m_listener.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName) 27 , 0 ); m_listener.Bind( new IPEndPoint(IPAddress.IPv6Any, 943 )); m_listener.Listen( 10 ); m_listener.BeginAccept( new AsyncCallback(OnConnection), null ); } public void OnConnection(IAsyncResult res) { Socket client = null ; try { client = m_listener.EndAccept(res); } catch (SocketException) { return ; } // handle this policy request with a PolicyConnection PolicyConnection pc = new PolicyConnection(client, m_policy); // look for more connections m_listener.BeginAccept( new AsyncCallback(OnConnection), null ); } public void Close() { m_listener.Close(); }} public class Program{ static void Main( string [] args) { if (args.Length == 0 ) { Console.WriteLine( " usage: PolicyServer.exe PolicyFile.xml " ); return ; } PolicyServer ps = new PolicyServer(args[ 0 ]); System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite); }}} 此路不通之后,又想起使用HttpListener类,看看是否能够监听http请求,果然能够截获HTTP的请求。
HttpListener listener = new HttpListener();listener.Prefixes.Add(http: // localhost/); listener.Start();Console.WriteLine( " 开始监听… " );HttpListenerContext context = listener.GetContext();HttpListenerRequest request = context.Request;HttpListenerResponse response = context.Response; 但是这种方式有个明显的缺点,就是线程是阻塞的。于是,又想到使用线程池。
System.Threading.ThreadPool.QueueUserWorkItem( new System.Threading.WaitCallback(Listen)); private static void Listen( object state){ while (httpListener.IsListening) { httpListener.BeginGetContext( new AsyncCallback(ListenerCallback), httpListener); listenForNextRequest.WaitOne(); }} 这样的话,每接收一个请求便会异步处理这个请求。在请求的处理上,接收请求后需要向外输出策略文件流,供silverlight端接收验证。
using (System.Net.HttpListenerResponse response = context.Response) { System.Threading.Thread.Sleep( 1000 ); string responseString = " <?xml version=\ " 1.0 \ " encoding=\ " utf - 8 \ " ?> " + " <access-policy> " + " <cross-domain-access> " + " <policy> " + " <allow-from http-request-headers=\ " * \ " > " + " <domain uri=\ " * \ " /> " + " </allow-from> " + " <grant-to> " + " <socket-resource port=\ " 4502 - 4534 \ " protocol=\ " tcp\ " /> " + " </grant-to> " + " </policy> " + " </cross-domain-access> " + " </access-policy> " ; byte [] buffer = System.Text.Encoding.UTF8.GetBytes(responseString); response.ContentLength64 = buffer.LongLength; response.OutputStream.Write(buffer, 0 , buffer.Length); } 启动这个模拟服务,将clientaccesspolicy从wwwroot中移除后再运行一下程序,OK,我们不再需要将策略文件放到IIS下了。
提醒
如果你的机器装了IIS,请还是放一个策略文件到wwwroot吧,否则就停掉IIS再使用这个类,因为IIS和这个类只能有一方监听80端口。
本文中的这个类参考了: