通过压缩SOAP改善XML Web service性能

压缩文本是一个可以减少文本内容尺寸达80%的过程。这意味着存储压缩的文本将会比存储没有压缩的文本少80%的空间。也意味着在网络上传输内容需要更少的时间,对于使用文本通信的客户端服务器应用程序来说,将会表现出更高的效率,例如XML Web services。

本文的主要目的就是寻找在客户端和服务器之间使交换的数据尺寸最小化的方法。一些有经验的开发者会使用高级的技术来优化通过网络特别是互联网传送的数据,这样的做法在许多分布式系统中都存在瓶颈。解决这个问题的一个方法是获取更多的带宽,但这是不现实的。另一个方法是通过压缩的方法使得被传输的数据达到最小。

当内容是文本的时候,通过压缩,它的尺寸可以减少80%。这就意味着在客户端和服务器之间带宽的需求也可以减少类似的百分比。为了压缩和解压缩,服务端和客户端则占用了CPU的额外资源。但升级服务器的CPU一般都会比增加带宽便宜,所以压缩是提高传输效率的最有效的方法。

XML/SOAP在网络中

让我们仔细看看SOAP在请求或响应XML Web service的时候,是什么在网络上传输。我们创建一个XML Web service,它包含一个 add 方法。这个方法有两个输入参数并返回这两个数的和:

  1<webmethod()> Public Function add(ByVal a As Integer, ByVal b As _Integer) As Integer   
  2add = a + b   
  3End Function 
  4
  5当 XML Web service 消费端调用这个方法的时候,它确实发送了一个SOAP请求到服务器: 
  6
  7<?xml version="1.0" encoding="utf-8"?>
  8<soap:envelope xmlns:soap=" http://schemas.xmlsoap.org/soap/envelope/ " xmlns:xsd=" http://www.w3.org/2001/XMLSchema " xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance ">
  9<soap:body><add xmlns=" http://tempuri.org/"><a>10</a><b>20</b></add>
 10</soap:body></soap:envelope>
 11
 12服务端使用一个SOAP响应来回应这个SOAP请求: 
 13
 14<?xml version="1.0" encoding="utf-8"?>
 15<soap:envelope xmlns:soap=" http://schemas.xmlsoap.org/soap/envelope/ " xmlns:xsd=" http://www.w3.org/2001/XMLSchema " xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance ">
 16<soap:body><addresponse xmlns=" http://tempuri.org/ ">
 17<addresult>30</addresult></addresponse>
 18</soap:body></soap:envelope>
 19
 20这是调用XML Web service的方法后,在网络上传输的实际信息。在更复杂的XML Web service中,SOAP响应可能是一个很大的数据集。例如,当Northwind中的表orders中的内容被序列化为XML后,数据可能达到454KB。如果我们创建一个应用通过XML Web service来获取这个数据集,那么SOAP响应将会包含所有的数据。 
 21
 22为了提高效率,在传输之前,我们可以压缩这些文本内容。我们怎样才能做到呢?当然是使用SOAP扩展! 
 23
 24SOAP 扩展 
 25
 26SOAP扩展是ASP.NET的Web方法调用的一个拦截机制,它能够在SOAP请求或响应被传输之前操纵它们。开发者可以写一段代码在这些消息序列化之前和之后执行。(SOAP扩展提供底层的API来实现各种各样的应用。) 
 27
 28使用SOAP扩展,当客户端从XML Web service调用一个方法的时候,我们能够减小SOAP信息在网络上传输的尺寸。许多时候,SOAP请求要比SOAP响应小很多(例如,一个大的数据集),因此在我们的例子中,仅对SOAP响应进行压缩。就像你在图1中所看到的,在服务端,当SOAP响应被序列化后,它会被压缩,然后传输到网络上。在客户端,SOAP信息反序列化之前,为了使反序列化成功,SOAP信息会被解压缩。   
 29
 30
 31![](http://dev.csdn.net/Develop/ArticleImages/24/24920/CSDN_Dev_Image_2004-2-271115261.gif)
 32
 33图 1. SOAP信息在序列化后被压缩(服务端),在反序列化前被解压缩(客户端) 
 34
 35我们也可以压缩SOAP请求,但在这个例子中,这样做的效率增加是不明显的。 
 36
 37为了压缩我们的Web service的SOAP响应,我们需要做两件事情: 
 38
 39· 在服务端序列化SOAP响应信息后压缩它。   
 40· 在客户端反序列化SOAP信息前解压缩它。 
 41
 42这个工作将由SOAP扩展来完成。在下面的段落中,你可以看到所有的客户端和服务端的代码。 
 43
 44首先,这里是一个返回大数据集的XML Web service: 
 45
 46Imports System.Web.Services   
 47<webservice(namespace )="" :=" http://tempuri.org/ "> _   
 48Public Class Service1   
 49Inherits System.Web.Services.WebService 
 50
 51<webmethod()> Public Function getorders() As DataSet   
 52Dim OleDbConnection1 = New System.Data.OleDb.OleDbConnection()   
 53OleDbConnection1.ConnectionString = "Provider=SQLOLEDB.1; _   
 54Integrated Security=SSPI;Initial Catalog=Northwind; _   
 55Data Source=.;Workstation ID=T-MNIKIT;"   
 56Dim OleDbCommand1 = New System.Data.OleDb.OleDbCommand()   
 57OleDbCommand1.Connection = OleDbConnection1   
 58OleDbConnection1.Open()   
 59Dim OleDbDataAdapter1 = New System.Data.OleDb.OleDbDataAdapter()   
 60OleDbDataAdapter1.SelectCommand = OleDbCommand1   
 61OleDbCommand1.CommandText = "Select * from orders"   
 62Dim objsampleset As New DataSet()   
 63OleDbDataAdapter1.Fill(objsampleset, "Orders")   
 64OleDbConnection1.Close()   
 65Return objsampleset   
 66End Function 
 67
 68End Class   
 69
 70
 71  
 72在客户端,我们构建了一个Windows应用程序来调用上面的XML Web service,获取那个数据集并显示在DataGrid中: 
 73
 74Public Class Form1   
 75Inherits System.Windows.Forms.Form   
 76'This function invokes the XML Web service, which returns the dataset   
 77'without using compression.   
 78Private Sub Button1_Click(ByVal sender As System.Object, _   
 79ByVal e As System.EventArgs) Handles Button1.Click   
 80Dim ws As New wstest.Service1()   
 81Dim test1 As New ClsTimer()   
 82'Start time counting…   
 83test1.StartTiming()   
 84'Fill datagrid with the dataset   
 85DataGrid1.DataSource = ws.getorders()   
 86test1.StopTiming()   
 87'Stop time counting…   
 88TextBox5.Text = "Total time: " &amp; test1.TotalTime.ToString &amp; "msec"   
 89End Sub 
 90
 91'This function invokes the XML Web service, which returns the dataset   
 92'using compression.   
 93Private Sub Button2_Click(ByVal sender As System.Object, _   
 94ByVal e As System.EventArgs) Handles Button2.Click   
 95Dim ws As New wstest2.Service1()   
 96Dim test1 As New ClsTimer()   
 97'Start time counting…   
 98test1.StartTiming()   
 99'Fill datagrid with dataset   
100DataGrid1.DataSource = ws.getorders()   
101test1.StopTiming()   
102'Stop time counting…   
103TextBox4.Text = "Total time: " &amp; test1.TotalTime.ToString &amp; "msec"   
104End Sub 
105
106End Class   
107  
108客户端调用了两个不同的XML Web services, 仅其中的一个使用了SOAP压缩。下面的Timer类是用来计算调用时间的: 
109
110Public Class ClsTimer   
111' Simple high resolution timer class   
112'   
113' Methods:   
114' StartTiming reset timer and start timing   
115' StopTiming stop timer   
116'   
117'Properties   
118' TotalTime Time in milliseconds   
119'Windows API function declarations   
120Private Declare Function timeGetTime Lib "winmm" () As Long 
121
122'Local variable declarations   
123Private lngStartTime As Integer   
124Private lngTotalTime As Integer   
125Private lngCurTime As Integer 
126
127Public ReadOnly Property TotalTime() As String   
128Get   
129TotalTime = lngTotalTime   
130End Get   
131End Property 
132
133Public Sub StartTiming()   
134lngTotalTime = 0   
135lngStartTime = timeGetTime()   
136End Sub 
137
138Public Sub StopTiming()   
139lngCurTime = timeGetTime()   
140lngTotalTime = (lngCurTime - lngStartTime)   
141End Sub   
142End Class   
143  
144服务端的SOAP扩展 
145
146在服务端,为了减小SOAP响应的尺寸,它被压缩。下面这段告诉你怎么做: 
147
148第一步 
149
150使用Microsoft Visual Studio .NET, 我们创建一个新的Visual Basic .NET 类库项目(使用"ServerSoapExtension"作为项目名称),添加下面的类: 
151
152Imports System   
153Imports System.Web.Services   
154Imports System.Web.Services.Protocols   
155Imports System.IO   
156Imports zipper 
157
158Public Class myextension   
159Inherits SoapExtension   
160Private networkStream As Stream   
161Private newStream As Stream 
162
163Public Overloads Overrides Function GetInitializer(ByVal _   
164methodInfo As LogicalMethodInfo, _   
165ByVal attribute As SoapExtensionAttribute) As Object   
166Return System.DBNull.Value   
167End Function 
168
169Public Overloads Overrides Function GetInitializer(ByVal _   
170WebServiceType As Type) As Object   
171Return System.DBNull.Value   
172End Function 
173
174Public Overrides Sub Initialize(ByVal initializer As Object)   
175End Sub 
176
177Public Overrides Sub ProcessMessage(ByVal message As SoapMessage)   
178Select Case message.Stage 
179
180Case SoapMessageStage.BeforeSerialize 
181
182Case SoapMessageStage.AfterSerialize   
183AfterSerialize(message) 
184
185Case SoapMessageStage.BeforeDeserialize   
186BeforeDeserialize(message) 
187
188Case SoapMessageStage.AfterDeserialize 
189
190Case Else   
191Throw New Exception("invalid stage")   
192End Select   
193End Sub 
194
195' Save the stream representing the SOAP request or SOAP response into a   
196' local memory buffer.   
197Public Overrides Function ChainStream(ByVal stream As Stream) As Stream   
198networkStream = stream   
199newStream = New MemoryStream()   
200Return newStream   
201End Function 
202
203' Write the compressed SOAP message out to a file at   
204'the server's file system..   
205Public Sub AfterSerialize(ByVal message As SoapMessage)   
206newStream.Position = 0   
207Dim fs As New FileStream("c:\temp\server_soap.txt", _   
208FileMode.Append, FileAccess.Write)   
209Dim w As New StreamWriter(fs)   
210w.WriteLine("-----Response at " + DateTime.Now.ToString())   
211w.Flush()   
212'Compress stream and save it to a file   
213Comp(newStream, fs)   
214w.Close()   
215newStream.Position = 0   
216'Compress stream and send it to the wire   
217Comp(newStream, networkStream)   
218End Sub 
219
220' Write the SOAP request message out to a file at the server's file system.   
221Public Sub BeforeDeserialize(ByVal message As SoapMessage)   
222Copy(networkStream, newStream)   
223Dim fs As New FileStream("c:\temp\server_soap.txt", _   
224FileMode.Create, FileAccess.Write)   
225Dim w As New StreamWriter(fs)   
226w.WriteLine("----- Request at " + DateTime.Now.ToString())   
227w.Flush()   
228newStream.Position = 0   
229Copy(newStream, fs)   
230w.Close()   
231newStream.Position = 0   
232End Sub 
233
234Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)   
235Dim reader As New StreamReader(fromStream)   
236Dim writer As New StreamWriter(toStream)   
237writer.WriteLine(reader.ReadToEnd())   
238writer.Flush()   
239End Sub 
240
241Sub Comp(ByVal fromStream As Stream, ByVal toStream As Stream)   
242Dim reader As New StreamReader(fromStream)   
243Dim writer As New StreamWriter(toStream)   
244Dim test1 As String   
245Dim test2 As String   
246test1 = reader.ReadToEnd   
247'String compression using NZIPLIB   
248test2 = zipper.Class1.Compress(test1)   
249writer.WriteLine(test2)   
250writer.Flush()   
251End Sub 
252
253End Class 
254
255' Create a SoapExtensionAttribute for the SOAP extension that can be   
256' applied to an XML Web service method.   
257<attributeusage(attributetargets.method)> _   
258Public Class myextensionattribute   
259Inherits SoapExtensionAttribute 
260
261Public Overrides ReadOnly Property ExtensionType() As Type   
262Get   
263Return GetType(myextension)   
264End Get   
265End Property 
266
267Public Overrides Property Priority() As Integer   
268Get   
269Return 1   
270End Get   
271Set(ByVal Value As Integer)   
272End Set   
273End Property 
274
275End Class   
276
277
278  
279第二步 
280
281我们增加ServerSoapExtension.dll程序集作为引用,并且在web.config声明SOAP扩展: 
282
283<?xml version="1.0" encoding="utf-8" ?>
284<configuration>
285<system.web>
286<webservices>
287<soapextensiontypes>
288<add group="0" priority="1" type="ServerSoapExtension.myextension,   
289ServerSoapExtension"></add>
290</soapextensiontypes>
291</webservices>
292
293...   
294</system.web>
295</configuration>   
296  
297就象你在代码中看到的那样,我们使用了一个临时目录("c:\temp")来捕获SOAP请求和压缩过的SOAP响应到文本文件("c:\temp\server_soap.txt")中 。 
298
299客户端的SOAP扩展 
300
301在客户端,从服务器来的SOAP响应被解压缩,这样就可以获取原始的响应内容。下面就一步一步告诉你怎么做: 
302
303第一步 
304
305使用Visual Studio .NET, 我们创建一个新的Visual Basic .NET类库项目(使用 "ClientSoapExtension"作为项目名称),并且添加下面的类: 
306
307Imports System   
308Imports System.Web.Services   
309Imports System.Web.Services.Protocols   
310Imports System.IO   
311Imports zipper   
312Public Class myextension   
313Inherits SoapExtension 
314
315Private networkStream As Stream   
316Private newStream As Stream 
317
318Public Overloads Overrides Function GetInitializer(ByVal _   
319methodInfo As LogicalMethodInfo, _   
320ByVal attribute As SoapExtensionAttribute) As Object   
321Return System.DBNull.Value   
322End Function 
323
324Public Overloads Overrides Function GetInitializer(ByVal _   
325WebServiceType As Type) As Object   
326Return System.DBNull.Value   
327End Function 
328
329Public Overrides Sub Initialize(ByVal initializer As Object)   
330End Sub 
331
332Public Overrides Sub ProcessMessage(ByVal message As SoapMessage)   
333Select Case message.Stage 
334
335Case SoapMessageStage.BeforeSerialize 
336
337Case SoapMessageStage.AfterSerialize   
338AfterSerialize(message) 
339
340Case SoapMessageStage.BeforeDeserialize   
341BeforeDeserialize(message) 
342
343Case SoapMessageStage.AfterDeserialize 
344
345Case Else   
346Throw New Exception("invalid stage")   
347End Select   
348End Sub 
349
350' Save the stream representing the SOAP request or SOAP response   
351' into a local memory buffer.   
352Public Overrides Function ChainStream(ByVal stream As Stream) _   
353As Stream   
354networkStream = stream   
355newStream = New MemoryStream()   
356Return newStream   
357End Function 
358
359' Write the SOAP request message out to a file at   
360' the client's file system.   
361Public Sub AfterSerialize(ByVal message As SoapMessage)   
362newStream.Position = 0   
363Dim fs As New FileStream("c:\temp\client_soap.txt", _   
364FileMode.Create, FileAccess.Write)   
365Dim w As New StreamWriter(fs)   
366w.WriteLine("----- Request at " + DateTime.Now.ToString())   
367w.Flush()   
368Copy(newStream, fs)   
369w.Close()   
370newStream.Position = 0   
371Copy(newStream, networkStream)   
372End Sub 
373
374' Write the uncompressed SOAP message out to a file   
375' at the client's file system..   
376Public Sub BeforeDeserialize(ByVal message As SoapMessage)   
377'Decompress the stream from the wire   
378DeComp(networkStream, newStream)   
379Dim fs As New FileStream("c:\temp\client_soap.txt", _   
380FileMode.Append, FileAccess.Write)   
381Dim w As New StreamWriter(fs)   
382w.WriteLine("-----Response at " + DateTime.Now.ToString())   
383w.Flush()   
384newStream.Position = 0   
385'Store the uncompressed stream to a file   
386Copy(newStream, fs)   
387w.Close()   
388newStream.Position = 0   
389End Sub 
390
391Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)   
392Dim reader As New StreamReader(fromStream)   
393Dim writer As New StreamWriter(toStream)   
394writer.WriteLine(reader.ReadToEnd())   
395writer.Flush()   
396End Sub 
397
398Sub DeComp(ByVal fromStream As Stream, ByVal toStream As Stream)   
399Dim reader As New StreamReader(fromStream)   
400Dim writer As New StreamWriter(toStream)   
401Dim test1 As String   
402Dim test2 As String   
403test1 = reader.ReadToEnd   
404'String decompression using NZIPLIB   
405test2 = zipper.Class1.DeCompress(test1)   
406writer.WriteLine(test2)   
407writer.Flush()   
408End Sub 
409
410End Class 
411
412' Create a SoapExtensionAttribute for the SOAP extension that can be   
413' applied to an XML Web service method.   
414<attributeusage(attributetargets.method)> _   
415Public Class myextensionattribute   
416Inherits SoapExtensionAttribute 
417
418Public Overrides ReadOnly Property ExtensionType() As Type   
419Get   
420Return GetType(myextension)   
421End Get   
422End Property 
423
424Public Overrides Property Priority() As Integer   
425Get   
426Return 1   
427End Get   
428Set(ByVal Value As Integer)   
429End Set   
430End Property 
431
432End Class   
433  
434就象你在代码中看到的那样,我们使用了一个临时目录("c:\temp") 来捕获SOAP请求和解压缩的SOAP响应到文本文件("c:\temp\client_soap.txt")中。 
435
436第二步 
437
438我们添加ClientSoapExtension.dll程序集作为引用,并且在我们的应用程序的XML Web service引用中声明SOAP扩展: 
439
440'-------------------------------------------------------------------------   
441' <autogenerated>   
442' This code was generated by a tool.   
443' Runtime Version: 1.0.3705.209   
444'   
445' Changes to this file may cause incorrect behavior and will be lost if   
446' the code is regenerated.   
447' </autogenerated>   
448'-------------------------------------------------------------------------   
449Option Strict Off   
450Option Explicit On 
451
452Imports System   
453Imports System.ComponentModel   
454Imports System.Diagnostics   
455Imports System.Web.Services   
456Imports System.Web.Services.Protocols   
457Imports System.Xml.Serialization 
458
459'   
460'This source code was auto-generated by Microsoft.VSDesigner,   
461'Version 1.0.3705.209.   
462'   
463Namespace wstest2 
464
465'<remarks></remarks>
466<system.diagnostics.debuggerstepthroughattribute(), )="" ,="" [namespace]:=" http://tempuri.org/ " _="" system.componentmodel.designercategoryattribute("code"),="" system.web.services.webservicebindingattribute(name:="Service1Soap"> _   
467Public Class Service1   
468Inherits System.Web.Services.Protocols.SoapHttpClientProtocol 
469
470'<remarks></remarks>   
471Public Sub New()   
472MyBase.New   
473Me.Url = " http://localhost/CompressionWS/Service1.asmx "   
474End Sub 
475
476'<remarks></remarks>
477<system.web.services.protocols.soapdocumentmethodattribute "="" ",="" ("="" ,="" .wrapped),="" :="_" _="" clientsoapextension.myextensionattribute()="" getproducts",requestnamespace="" http:="" parameterstyle:="System.Web.Services.Protocols.SoapParameterStyle" responsenamespace:=" http://tempuri.org/ " tempuri.org="" use:="System.Web.Services.Description.SoapBindingUse.Literal,_"> _   
478Public Function getproducts() As System.Data.DataSet   
479Dim results() As Object = Me.Invoke("getproducts", _   
480New Object(-1) {})   
481Return CType(results(0), System.Data.DataSet)   
482End Function 
483
484'<remarks></remarks>   
485Public Function Begingetproducts(ByVal callback As _   
486System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult _ 
487
488Return Me.BeginInvoke("getproducts", New Object(-1) {}, _   
489callback, asyncState)   
490End Function 
491
492'<remarks></remarks>   
493Public Function Endgetproducts(ByVal asyncResult As _   
494System.IAsyncResult) As System.Data.DataSet   
495Dim results() As Object = Me.EndInvoke(asyncResult)   
496Return CType(results(0), System.Data.DataSet)   
497End Function   
498End Class   
499End Namespace   
500
501
502  
503这里是zipper类的源代码,它是使用免费软件NZIPLIB库实现的: 
504
505using System;   
506using NZlib.GZip;   
507using NZlib.Compression;   
508using NZlib.Streams;   
509using System.IO;   
510using System.Net;   
511using System.Runtime.Serialization;   
512using System.Xml;   
513namespace zipper   
514{   
515public class Class1   
516{   
517public static string Compress(string uncompressedString)   
518{   
519byte[] bytData = System.Text.Encoding.UTF8.GetBytes(uncompressedString);   
520MemoryStream ms = new MemoryStream();   
521Stream s = new DeflaterOutputStream(ms);   
522s.Write(bytData, 0, bytData.Length);   
523s.Close();   
524byte[] compressedData = (byte[])ms.ToArray();   
525return System.Convert.ToBase64String(compressedData, 0, _   
526compressedData.Length);   
527} 
528
529public static string DeCompress(string compressedString)   
530{   
531string uncompressedString="";   
532int totalLength = 0;   
533byte[] bytInput = System.Convert.FromBase64String(compressedString);;   
534byte[] writeData = new byte[4096];   
535Stream s2 = new InflaterInputStream(new MemoryStream(bytInput));   
536while (true)   
537{   
538int size = s2.Read(writeData, 0, writeData.Length);   
539if (size &gt; 0)   
540{   
541totalLength += size;   
542uncompressedString+=System.Text.Encoding.UTF8.GetString(writeData, _   
5430, size);   
544}   
545else   
546{   
547break;   
548}   
549}   
550s2.Close();   
551return uncompressedString;   
552}   
553}   
554}   
555
556
557  
558分析 
559
560软件&amp;硬件 
561
562· 客户方: Intel Pentium III 500 MHz, 512 MB RAM, Windows XP.   
563· 服务器方: Intel Pentium III 500 MHz, 512 MB RAM, Windows 2000 Server, Microsoft SQL Server 2000. 
564
565在客户端,一个Windows应用程序调用一个XML Web service。这个XML Web service 返回一个数据集并且填充客户端应用程序中的DataGrid。 
566
567![](http://dev.csdn.net/Develop/ArticleImages/24/24920/CSDN_Dev_Image_2004-2-271115263.jpg)
568
569图2. 这是一个我们通过使用和不使用SOAP压缩调用相同的XML Web Service的样例程序。这个XML Web service返回一个大的数据集。 
570
571CPU 使用记录 
572
573就象在图3中显示的那样,没有使用压缩的CPU使用时间是 29903 milliseconds. 
574
575![](http://dev.csdn.net/Develop/ArticleImages/24/24920/CSDN_Dev_Image_2004-2-271115265.gif)
576
577图 3. 没有使用压缩的CPU使用记录 
578
579在我们的例子中,使用压缩的CPU使用时间显示在图4中, 是15182 milliseconds. 
580
581![](http://dev.csdn.net/Develop/ArticleImages/24/24920/CSDN_Dev_Image_2004-2-271115267.gif)
582
583图 4. 使用压缩的CPU 使用记录 
584
585正如你看到的,我们在客户方获取这个数据集的时候,使用压缩与不使用压缩少用了近50%的CPU时间,仅仅在CPU加载时有一点影响。当客户端和服务端交换大的数据时,SOAP压缩能显著地增加XML Web Services效率。 在Web中,有许多改善效率的解决方案。我们的代码是获取最大成果但是最便宜的解决方案。SOAP扩展是通过压缩交换数据来改善XML Web Services性能的,这仅仅对CPU加载时间造成了一点点影响。并且值得提醒的是,大家可以使用更强大的和需要更小资源的压缩算法来获取更大的效果</system.web.services.protocols.soapdocumentmethodattribute></system.diagnostics.debuggerstepthroughattribute(),></attributeusage(attributetargets.method)></attributeusage(attributetargets.method)></webmethod()></webservice(namespace></webmethod()>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus