Deep♂In WCF

WCF全称Windows Communication Foundation, 是微软爸爸开发出的用于构建面向服务的应用程序的统一编程模型。用于替代和统一.net remoting和web service。

WCF大致是以web service那个基于xml的soap为基础,又除http/https外同时支持tcp(net.tcp)、udp(net.udp)、msmq、named pipe这些通讯方式。也提供了json格式对象序列化方式(好像默认仅限web?)。当然实际上还支持扩展(比较麻烦),比如说把序列化协议改成protobuff。

Deep♂Dark♂Fantastic

举个栗子,这是http通讯方式下。

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://tempuri.org/IDateService/FetchDateTime</a:Action>
    <a:MessageID>urn:uuid:d8339919-5fd5-4579-b99d-10eb635c3749</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
  </s:Header>
  <s:Body>
    <FetchDateTime xmlns="http://tempuri.org/" />
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://tempuri.org/IDateService/FetchDateTimeResponse</a:Action>
    <a:RelatesTo>urn:uuid:1401480b-4c99-4e46-a391-fe8cdd0bddad</a:RelatesTo>
  </s:Header>
  <s:Body>
    <FetchDateTimeResponse xmlns="http://tempuri.org/">
      <FetchDateTimeResult>2018-04-16T13:17:21.2341923+08:00</FetchDateTimeResult>
    </FetchDateTimeResponse>
  </s:Body>
</s:Envelope>
   M<+         ?         2 \Device\NPF_{88D4FAB5-D621-475E-8706-637A09F5DB15}  	      
  port 8066   + 64-bit Windows 7 Service Pack 1, build 7601     ?     d       餴 #NTB   B    BZ8<?
	燿 E  4?譆 €  ?#m?V8鷡倻幔?   €  邀  ?  d      d       餴 %NTB   B   
	燿 BZ8<? E  4[@ }敩V8?#m傶~"靧覝幔葊  壛  ?  d      X       餴 CNT6   6    BZ8<?
	燿 E  (?谸 €  ?#m?V8鷡倻幔?靧覲@)扬    X      $      餴 ?NT     BZ8<?
	燿 E  ?貮 €  ?#m?V8鷡倻幔?靧覲@)医  POST /DateService/ HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
Host: 172.16.86.56:8066
Content-Length: 526
Expect: 100-continue
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

$     p       餴 ?NTO   O   
	燿 BZ8<? E  A\@ }啲V8?#m傶~"靧訙幛朠 矑  HTTP/1.1 100 Continue

 p      d      餴 ?NTD  D   BZ8<?
	燿 E 6?贎 €  ?#m?V8鷡倻幛?靧霵@"育  <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"><s:Header><a:Action s:mustUnderstand="1">http://tempuri.org/IDateService/FetchDateTime</a:Action><a:MessageID>urn:uuid:1401480b-4c99-4e46-a391-fe8cdd0bddad</a:MessageID><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand="1">http://172.16.86.56:8066/DateService/</a:To></s:Header><s:Body><FetchDateTime xmlns="http://tempuri.org/"/></s:Body></s:Envelope>d     ?      餴 ?NT?  ?  
	燿 BZ8<? E ?]@ }!?V8?#m傶~"靧鞙幡 ? HTTP/1.1 200 OK
Content-Length: 478
Content-Type: application/soap+xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 16 Apr 2018 05:17:21 GMT

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"><s:Header><a:Action s:mustUnderstand="1">http://tempuri.org/IDateService/FetchDateTimeResponse</a:Action><a:RelatesTo>urn:uuid:1401480b-4c99-4e46-a391-fe8cdd0bddad</a:RelatesTo></s:Header><s:Body><FetchDateTimeResponse xmlns="http://tempuri.org/"><FetchDateTimeResult>2018-04-16T13:17:21.2341923+08:00</FetchDateTimeResult></FetchDateTimeResponse></s:Body></s:Envelope> ?     X       餴 ?QT6   6    BZ8<?
	燿 E  (?跕 €  ?#m?V8鷡倻幡?靫iP?冄?   X

可以看到这是非常标准的soap的方式。而tcp模式下,微软爸爸则对这些soap这套繁复的东西进行了压缩。所以这个东西就被命名成net.tcp了,也只有.net客户端可以调用,微软爸爸把序列化反序列化方法都隐藏了。大概在 System.ServiceModel.Channels.BinaryMessageEncoderFactory 、 System.ServiceModel.Channels.BinaryFormatBuilder 、 System.ServiceModel.Channels.BinaryFormatParser 中。具体就长这样。

   M<+         ?         2 \Device\NPF_{435FEB21-F35A-400C-BFFF-281D55F2EE9B}  	      '  tcp dst port 8818 or tcp src port 8818  + 64-bit Windows 7 Service Pack 1, build 7601     ?     d       汣 g虲B   B    ?X蠵橺〞 E  43珸 €  ?R:?V8织"r湜    €   ? ?  d      `       汣 €蜟>   >   蠵橺〞 ?X E  0A瓳 ~簡?V8?R:"r织囲?湝p  雝  ?  `      X       汣 挛C6   6    ?X蠵橺〞 E  (3瑻 €  ?R:?V8织"r湝囲?P ?   X      ?      汣 螩l   l    ?X蠵橺〞 E  ^3瓳 €  ?R:?V8织"r湝囲?P ?   ,net.tcp://172.16.86.56:8818/Tcp/ShareManager?     \       汣 轮C<   <   蠵橺〞 ?X E  )A疈 ~簩?V8?R:"r织囲?溡P2	       \      \      汣 e淓;  ;   ?X蠵橺〞 E -3聾 €  ?R:?V8织"r溡囲?P? ??*http://tempuri.org/IShareManager/GetShares,net.tcp://172.16.86.56:8818/Tcp/ShareManager	GetShareshttp://tempuri.org/
customerNo	pageIndexpageCaptionVsaVD
 偒D?Y?N㎡秊5??D,D*?D 偒VB
B	?1c2c67c137d249fc9c84293a4aa00026B傿
? \     \       汣 鈭H<   <   蠵橺〞 ?X E  (A菮 ~簍?V8?R:"r织囲?澴P=        \            汣 K芅?  ?  蠵橺〞 ?X E 蹵這 ~穿?V8?R:"r织囲?澴P ? ??2http://tempuri.org/IShareManager/GetSharesResponseGetSharesResponsehttp://tempuri.org/GetSharesResult3http://schemas.datacontract.org/2004/07/Em.Entities)http://www.w3.org/2001/XMLSchema-instanceCodeMessagenil	ErrorCode
ErrorMessages9http://schemas.microsoft.com/2003/10/Serialization/ArraysMessage
OldMessageResultShare
CreateTime	IsEnabled
UpdateTime
CustomerNoAvailableVolAverageCost
BankCardNoBankCodeBusinFrozenVol
ChargeTypeDepositAccountNoEmFrozenVolEmRemark	FrozenVolFundCodeIdOnwayVol
OpenProfitTotalVolUnPaid
UnPaidDateUnitCostUnitValSucceedpreValueVsaVD
 偒D?Y?N㎡秊5??D 偒VB
Bb	iE
?E?E?cEEEEE?#O蒃炓E!嘐#?#O蒃炓E%?1c2c67c137d249fc9c84293a4aa00026E'?       ?      E)?               E+?6214850210872799E-?007E/?       d       E1E3?2000355387E5?               E7?E9?               E;?519588E=嵄iE??               EA?               EC?       ?      EE?               EG?       EI?               EK?               EE???椧E!嘐#???椧E%?1c2c67c137d249fc9c84293a4aa00026E'?       (#      E)?               E+?6214850210872799E-?007E/?       ?      E1E3?2000355387E5?               E7?E9?               E;?470028E=崑iE??               EA?               EC?       '      EE?               EG?       EI?               EK?               EE梹k椧E!嘐#梹k椧E%?1c2c67c137d249fc9c84293a4aa00026E'?       ?        ?      汣 てN?  ?  蠵橺〞 ?X E 郂 ~份?V8?R:"r织囲防澴P[1       E)?               E+?6214850210872799E-?007E/?               E1E3?2000355387E5?               E7?E9?               E;?540002E=崐iE??               EA?               EC?       ?      EE?               EG?       EI?               EK?               EE?寲?椧E!嘐#?寲?椧E%?1c2c67c137d249fc9c84293a4aa00026E'?       ?      E)?               E+?6214850210872799E-?007E/?       p      E1E3?2000355387E5?               E7?E9?               E;?660012E=崏iE??               EA?               EC?       '      EE?               EG?       EI?               EK?               EM嘐O?   ?     X       汣 势N6   6    ?X蠵橺〞 E  (3酅 €  ?R:?V8织"r澴囲?P ?   X      \       汣 x(	C<   <   蠵橺〞 ?X E  (}駺 ~~J?V8?R:"r织囲?澴P  .?       \

wireshark截出来很多乱码,只能将就着看了,实际上压缩思想大致是把soap消息的header部分大致改成key-value形式,而body部分则是先给出scheme,再给出数据,跟json相比减少了数组中对key的冗余。整体与json相比肯定是强了一点,然而与thrift和protobuf比压缩率还是不够看。

WCF还提供基于MSMQ的队列工作模式、(分布式)事务支持、P2P网络模型(???)、RSS(wcf-syndication,鬼知道为什么起这名字)等奇奇怪怪一大堆功能(微软全家桶,真香)。(这些我都没用过

Deep♂Dark♂Fantastic

因为每个接口都要求详细的接口调用日志,这种用postsharp的反射可以轻松实现(。然而公司不允许使用postsharp(迂腐),不过也有基于IMessage的实现方式。因为windows整个系统是基于消息的,WCF调用起来实际上内部是用反射实现的,这样就会有MethodCallMessage。这样大致上实现ContextAttribute特性,然后实现IContextProperty,再实现IMessageSink,就可以为所欲为了(baidu一下就有了。

这里讲另一种方法,假如又懒得实现aop的话,那就要在每个函数末尾添加log方法,然而每个函数又都要拼接一大堆参数又非常累。虽然我们可以通过调试信息堆栈帧拿到当前执行的函数信息,然而我却拿不到参数值,这就非常尴尬。(虽然我觉得可以用WinAPI的ReadProcessMemory胡来一下,但这个还是得冷静一下

MethodBase mb = (new StackFrame(1, true)).GetMethod();
var pts = mb.GetParameters();
foreach (var pt in pts)
{
       dict.Add(pt.Name, "");//只能赋个空值
}

然而我发现微软贴心的在WCF的OperationContext中包含了RequestMessage,原始的soap消息。这样我们就可以这么做。

public static Dictionary<string, string> GetWcfRequestInfo()
{

    //<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    //  <s:Header>
    //    <a:Action s:mustUnderstand="1">http://tempuri.org/IRealTAPay/RealTAPayResultQry</a:Action>
    //    <a:MessageID>urn:uuid:33fed715-f529-4349-b556-38b569307a39</a:MessageID>
    //    <a:ReplyTo>
    //      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    //    </a:ReplyTo>
    //    <a:To s:mustUnderstand="1">net.tcp://localhost:8860/RealTAPay</a:To>
    //  </s:Header>
    //  <s:Body>
    //    <RealTAPayResultQry xmlns="http://tempuri.org/">
    //      <customerno i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    //      </customerno>
    //      <appserialno i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    //      </appserialno>
    //      <payno i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    //      </payno>
    //    </RealTAPayResultQry>
    //  </s:Body>
    //</s:Envelope>


    Dictionary<string, string> ret = new Dictionary<string, string>();
    //Stopwatch sw = new Stopwatch();
    //sw.Start();
    try
    {
        try
        {
            var message = OperationContext.Current.RequestContext.RequestMessage.ToString();
            message = message.Substring(message.IndexOf("<s:Body>") + 8);
            message = message.Substring(0, message.IndexOf("</s:Body>"));

            //var doc = new XmlDocument();
            //doc.LoadXml(message);
            //var body = doc.GetElementsByTagName("s:Body").Item(0).InnerXml;
            //XDocument xdoc = XDocument.Parse(message);

            //dynamic dyobj = XmlDeserializer.Deserialize(body);

            dynamic dyobj = XmlDeserializer.Deserialize(message);
            var json = JsonHelper.ConvertObjToJson(dyobj);
            ret["request"] = json;

        }
        catch
        {
            var mb = (new StackFrame(1, true)).GetMethod();

            var pts = mb.GetParameters();

            foreach (var pt in pts)
            {
                ret.Add(pt.Name, "");
            }
        }
    }
    catch
    {

    }

    //sw.Stop();
    //ret["sw"] = sw.ElapsedTicks.ToString();
    return ret;
}
public class XmlDeserializer
{
    /// <summary>
    /// 反序列化xml到object
    /// https://www.codeproject.com/Tips/227139/Converting-XML-to-an-Dynamic-Object-using-ExpandoO
    /// </summary>
    /// <param name="xml"></param>
    /// <param name="node"></param>
    /// <returns></returns>
    public static dynamic Deserialize(string xml, XElement node = null)
    {
        if (String.IsNullOrWhiteSpace(xml) && node == null) return null;

        // If a file is not empty then load the xml and overwrite node with the
        // root element of the loaded document
        node = !String.IsNullOrWhiteSpace(xml) ? XDocument.Parse(xml).Root : node;

        IDictionary<String, dynamic> result = new ExpandoObject();

        // implement fix as suggested by [ndinges]
        PluralizationService pluralizationService =
            PluralizationService.CreateService(CultureInfo.CreateSpecificCulture("en-us"));

        // use parallel as we dont really care of the order of our properties
        node.Elements().AsParallel().ForAll(gn =>
        {
            // Determine if node is a collection container
            var isCollection = gn.HasElements &&
                (
                    // if multiple child elements and all the node names are the same
                    gn.Elements().Count() > 1 &&
                    gn.Elements().All(
                        e => e.Name.LocalName.ToLower() == gn.Elements().First().Name.LocalName) ||

                    // if there's only one child element then determine using the PluralizationService if
                    // the pluralization of the child elements name matches the parent node. 
                    gn.Name.LocalName.ToLower() == pluralizationService.Pluralize(
                        gn.Elements().First().Name.LocalName).ToLower()
                );

            // If the current node is a container node then we want to skip adding
            // the container node itself, but instead we load the children elements
            // of the current node. If the current node has child elements then load
            // those child elements recursively
            var items = isCollection ? gn.Elements().ToList() : new List<XElement>() { gn };

            var values = new List<dynamic>();

            // use parallel as we dont really care of the order of our properties
            // and it will help processing larger XMLs
            items.AsParallel().ForAll(i => values.Add((i.HasElements) ?
               Deserialize(null, i) : i.Value.Trim()));

            // Add the object name + value or value collection to the dictionary
            result[gn.Name.LocalName] = isCollection ? values : values.FirstOrDefault();
        });
        return result;
    }

}

在我本机上性能测试了一下,用字符串的方法取出body部分输出大概只需要1ms,取出body部分后把body转为json大概需要2ms,然而用System.Xml.Linq直接解析xml然后取body部门再转json需要4ms。可以说xml解析性能相当惨烈(实际上System.Xml.Linq应该背锅,这个解析比System.Xml慢

这里还用到了一个万能xml反序列化到object的方法,从codeproject上抄来的。(然而json却可以直接、简单的反序列到object。

Deep♂Dark♂Fantastic

关于WCF的性能,微软爸爸其实对WCF已经规划了很多种情况,见会话、实例与并发。但实际上我们大多数情况下只用到WCF简单的rpc功能,而且在分布式系统下,我们不能保证客户端到服务端的会话是固定,所以会话模式大多数情况下是不适用的,我们也可以参照restful的相关理念,在参数中加入token以实现类似效果。

这样的话,我们可以直接设定我们实例设定为单服务实例多线程并发模式。当然PreCall模式也是可以接受的,只不过每次会创建新的服务实例,应该有另外的开销(PreCall模式下设置ConcurrencyMode实际上也没有意义)。

[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple, InstanceContextMode=InstanceContextMode.Single)]
public class RpcService : IRpc
{   
    //...
}

除此之外我们还可以修改WCF默认的并发限制。具体参数文档

其中maxConcurrentCalls配置值在实际运行中会乘以CPU核数才是实际值。比如说默认值16在一台16核的服务器上的实际限制是同时最多有 16 * 16 = 256次调用。看起来是不是很少,但这里就是并发与并行的问题了。

<configuration>
  <appSettings>
  </appSettings>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceThrottling 
            maxConcurrentCalls="16" 
            maxConcurrentSessions="0" 
            maxConcurrentInstances="0"
          />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注