最近遇到这样一个场景:尽可能快地调用服务,快是首要目标,其他因素次之,甚至可忽略。在这种情况下哪种方式更符合场景需求呢?一般来说服务的调用都采用框架中客户端代理方式来实 现,但是这种方式不够快,经查阅一些资料了解到通过Socket的方式直接调用服务是最快的,下面探究一下Socket实现服务调用的过程。
无论是采用客户端代理方式还是Socket方式调用服务的基本过程都一样:客户端发送请求报文,服务器应答,客户端接收响应报文。不同的是框架中客户端代理方式在发送请求报文前和接收响应报文后做了很多其它额外处理,Socket方式则跳过了这些额外处理,直接发送构造好的请求报文,接收响应报文后直接做处理。相对来说Socket直接发送接收报文方式跳过了很大部分特性,如安全性、易用性、可扩展性等,具有很大的局限性,但是符合当前场景的要求。
2、分析
Socket调用服务涉及到几个要素是:请求报文、响应报文、发送和接收报文。其中发送和接收报文可以直接使用.NET框架中的Socket对象来实现,剩下的就是如何构造请求报文和解析响应报文的问题了。服务的请求响应协议有HTTP POST、SOAP1.1和SOAP1.2,每种协议的请求响应报文格式不尽相同的,其中HTTP POST协议的请求响应报文相对简单,但是相对来说有一定限制性,比如无法使用WCF框架的安全特性,不过可以使用HTTPS安全协议,另外,不能请求服务中带复杂类型的方法,所以一般默认的请求都是使用SOAP方式。下面看看请求和响应一个简单的整数相加WebService服务的各种协议请求报文格式。
HTTP POST请求WebService报文格式如下:
POST /CalculateService.asmx/Add HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded Content-Length: length
num1=string&num2=string |
SOAP1.1请求WebService报文格式如下:
POST /CalculateService.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://tempuri.org/Add"
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <Add xmlns="http://tempuri.org/"> <num1>int</num1> <num2>int</num2> </Add> </soap:Body> </soap:Envelope> |
SOAP1.2请求WebService报文格式如下:
POST /CalculateService.asmx HTTP/1.1 Host: localhost Content-Type: application/soap+xml; charset=utf-8 Content-Length: length
<?xml version="1.0" encoding="utf-8"?> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <Add xmlns="http://tempuri.org/"> <num1>int</num1> <num2>int</num2> </Add> </soap12:Body> </soap12:Envelope> |
请求和响应报文都表头和表体信息,而且表头和表体是通过空行分离和识别的,这空行在报文中尤为重要,去掉则会请求失败。其中请求报文中红色部分是依赖请求服务和数据,需要根据服务 地址和请求数据动态生成。这下知道报文格式就容易定义和构造请求报文和解析响应报文了。
另外WCF服务默认请求的报文与WebService的SOAP1.1请求报文除了WCF请求报文头中SOAPAction的值需要指定服务公开的接口名称一点不相同外,其他基本一致,见下图红色部分。
POST /CalculateInWcf.svc HTTP/1.1 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://tempuri.org/ICalculate/Add" Host: localhost:82 Content-Length: 163 Expect: 100-continue Accept-Encoding: gzip, deflate Connection: Keep-Alive
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><Add xmlns="http://tempuri.org/"><num1>11</num1><num2>22</num2></Add></s:Body></s:Envelope> |
3、实现
1) 首先根据报文格式来实现请求报文生成器;下面是SOAP1.1协议请求WebService服务报文生成器实现代码:
1 /// <summary> 2 /// 报文生成器 3 /// </summary> 4 public abstract class MessageGenerator<TRequestData> : IMessageGenerator 5 where TRequestData : RequestData 6 { 7 public static Encoding DefaultEncoding = Encoding.UTF8; 8 9 public Encoding MessageEncoding { get; private set; } 10 public abstract string MessageHeadTemplate { get; } 11 12 public MessageGenerator() 13 { 14 this.MessageEncoding = DefaultEncoding; 15 } 16 17 public MessageGenerator(Encoding messageEncoding) 18 { 19 this.MessageEncoding = messageEncoding; 20 } 21 22 public virtual string GenerateRequestMessage(TRequestData requestData) 23 { 24 string messageBody = GenerateRequestBodyMessage(requestData); 25 string messageHead = GenerateRequestHeadMessage(requestData, messageBody); 26 return string.Format("{0}{1}{2}", messageHead, Environment.NewLine, messageBody); 27 } 28 29 protected virtual string GenerateRequestHeadMessage(TRequestData requestData, string bodyMessage) 30 { 31 return MessageHeadTemplate.Replace("$url", requestData.ServiceUri.AbsolutePath) 32 .Replace("$host", string.Format("{0}:{1}", requestData.ServiceUri.Host, requestData.ServiceUri.Port)) 33 .Replace("$method", requestData.MethodName) 34 .Replace("$length", MessageEncoding.GetByteCount(bodyMessage).ToString()); 35 } 36 protected abstract string GenerateRequestBodyMessage(TRequestData requestData); 37 38 39 string IMessageGenerator.GenerateRequestMessage(object requestData) 40 { 41 return GenerateRequestMessage(requestData as TRequestData); 42 } 43 } 44 45 public class Soap1_1WSMessageGenerator : MessageGenerator<RequestData> 46 { 47 public static string Soap1_1MessageHeadTemplate = @"POST $url HTTP/1.1 48 Host: $host 49 Content-Type: text/xml; charset=utf-8 50 Content-Length: $length" + Environment.NewLine + 51 "SOAPAction: \"http://tempuri.org/$method\"" + Environment.NewLine; 52 53 public static string Soap1_1MessageBodyTemplate = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + 54 "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" + 55 "<soap:Body>" + 56 "<$method xmlns=\"http://tempuri.org/\">" + 57 "$param" + 58 "</$method>" + 59 "</soap:Body>" + 60 "</soap:Envelope>"; 61 62 public Soap1_1WSMessageGenerator() 63 : base() 64 { 65 } 66 67 public Soap1_1WSMessageGenerator(Encoding messageEncoding) 68 : base(messageEncoding) 69 { 70 } 71 72 #region MessageGenerator 实现 73 74 public override string MessageHeadTemplate 75 { 76 get 77 { 78 return Soap1_1MessageHeadTemplate; 79 } 80 } 81 82 protected override string GenerateRequestBodyMessage(RequestData requestData) 83 { 84 string paramInfo = string.Join(Environment.NewLine, requestData.ParameterNameValues.Select(item => string.Format("<{0}>{1}</{0}>", item.Key, item.Value))); 85 return Soap1_1MessageBodyTemplate.Replace("$method", requestData.MethodName) 86 .Replace("$param", paramInfo); 87 } 88 89 #endregion 90 }
其中方法GenerateRequestMessage根据请求服务数据(服务地址、方法名、参数名值键值对)来生成相应的请求报文。
2) 接着使用.NET的Socket对象来实现发送和接收报文;
1 ///<summary> 2 3 ///同步发送消息后返回接收的消息 4 5 ///</summary> 6 7 ///<param name="message"></param> 8 9 ///<returns></returns> 10 11 publicstring Send(string message) 12 13 { 14 15 Socket socket = GetSocket(); 16 17 try 18 19 { 20 21 byte[] messageByte = MessageEncoding.GetBytes(message); 22 23 socket.Send(messageByte); 24 25 26 27 SocketMessagePackageCollection messagePackageList = newSocketMessagePackageCollection(); 28 29 byte[] receiveBuffer = newbyte[BUFFER_SIZE]; 30 31 while (true) 32 33 { 34 35 int realReceiveLength = socket.Receive(receiveBuffer); 36 37 messagePackageList.AddPackage(newSocketMessagePackage(DateTime.Now, receiveBuffer, realReceiveLength)); 38 39 if (socket.Available == 0 || realReceiveLength < receiveBuffer.Length) 40 41 { 42 43 break; 44 45 } 46 47 } 48 49 byte[] recieveMessage = messagePackageList.GetMessageBytes().ToArray(); 50 51 return MessageEncoding.GetString(recieveMessage); 52 53 54 55 } 56 57 catch (Exception ex) 58 59 { 60 61 Debug.WriteLine(string.Format("同步发送报文出现异常:{0}", ex.Message)); 62 63 throw; 64 65 } 66 67 finally 68 69 { 70 71 socket.Close(); 72 73 } 74 75 }
3) 最后根据待调用的服务方法元数据和传入参数数据来构造请求实体,使用Socket发送接收报文即可。
1 Uri serviceUri = newUri(@"http://localhost:20915/CalculateService.asmx"); 2 3 RequestData requestData = newRequestData(serviceUri, "Add"); 4 5 requestData.AddParam("num1", "11"); 6 7 requestData.AddParam("num2", "11"); 8 9 10 11 Soap1_1WSMessageGenerator soap1_1RequestMessage = newSoap1_1WSMessageGenerator(); 12 13 SocketProxy proxy = newSocketProxy(requestData.ServiceUri); 14 15 string requestMessage = soap1_1RequestMessage.GenerateRequestMessage(requestData); 16 17 18 19 string responeResult = proxy.Send(requestMessage);
4、总结
使用Socket调用服务主要是构造请求报文,WebService和WCF服务不同的请求协议或配置下报文格式都不尽相同,所以这种方式对具体的服务依赖性很大,只针对特定场景下使用。比如服务增加了安全性配置对报文数据都进行加密处理,导致大幅度地增加构造请求报文复杂性,而且接收到的响应报文也做了加密处理,不解密的话也无法读取相应的结果数据。
附:代码地址