你的位置是:网站首页--- 技术文章---计算机技术

让伺服器程式可以随时更新,服务不打烊


【 字体:


       第一次开发分散式系统是用Delphi 4写COM+,当应用程式伺服器需要更新程式时必须请(或等待、强制)所有使用者离线,停止Windows的该元件服务,更新程式,重新启动元件服务。换了工作后用VB6写COM+,依附在IIS以支援Internet用户,当应用程式伺服器需要更新程式时同样必须请(或等待、强制)所有使用者离线,停止IIS,更新程式后重新启动IIS 。当这样的更新很频繁的时候,就成了软体服务商与使用者共同的梦魇,很不幸地为了满足不同客户随时提出的客制需求,我们的套装ERP系统就面临这样的困境。现在我要用.Net开发分散式系统,试者想找出不需停止服务就能更新程式的解决方案,但不管是用Web Service、.Net Remoting或以前的COM元件,最后都面临相同的问题,只要服务不停止,档案就会因为在使用中而无法更新,面对这个事实真的就束手无策了吗? 
       换个想法,有没有可能设计一个服务功能,这个服务可以提供任何服务,而「可以提供任何服务」的这个服务是不需要更新的,听起来好像在玩文字游戏,用个具体场景来说明好了,假设客户想申办任何事情,只要准备好资料全部装到一个牛皮纸袋,到指定的地方交给窗口人员告知需要什么服务,服务人员只要拿起一张表看看有没有这项服务,如果没有就拒收,如果有,也不管牛皮纸袋装什么就转给表上指定的承办单位处理,再把承办单位处理结果原封不动交给客户。对客户来说,这个服务窗口提供了所有服务,可是对服务单位来说,这个服务窗口其实只是一个收发室负责收、交件,不参与也不干涉各项服务,它永远只做一件相同的工作-做为承办单位与顾户沟通的桥梁,这个工作不管遇到什么样的服务内容都是适用的,这就是我所说的「可以提供任何服务」而且「不会改变」的一项服务。

       以程式设计来说,我们可以用二个介面来描述刚刚的场景,提供服务的承办单位就叫IRemoteService ,每个承办单位可以提供数项服务(需要一个识别值string id ),每项服务可能需要若干资料(object[] args),最后也有可能要给客户若干资料(回传object ),所以IRemoteService可以定为

public interface IRemoteService
{
object Execute(string id, object[] args);
}

       而负责收、交件的服务窗口对客户来说就是一个服务中心的接待室,只要告知需要那一个单位(string serviceName对应一个IRemoteService)的那一项服务(string serviceID对应IRemoteService的id),并把资料(object[] args对应IRemoteService的args )交给收办人员,最后取回承办结果(回传object对应IRemoteService的回传值),所以这个服务中心IRemoteServiceCenter可以定为

public interface IRemoteServiceCenter{
object ExecuteService(string serviceName, string serviceID, object[] args);
}

       根据刚刚的说明,我们知道这个服务中心的实作很简单,读取设定档,找serviceName对应的档案及物件类别,如果没有就告知用户端程式指定的服务名称不存在,如果有就从档案载入物件类别并建立执行个体,回传IRemoteService.Execute(serviceID, args)的结果,然后卸载服务档案好让服务档案可以更新。 IRemoteServiceCenter提供用户端任何服务功能,但本身不含任何服务项目的实作,不需要因为修改服务内容而更新,就算有新增或删除服务档案,也只要更新设定档,既不需要停止IRemoteServiceCenter的服务也不需要重新在作业系统注册(不管是COM或.Net Remoting都要对发行的元件做注册),因为现在伺服器唯一要注册发行的元件只有IRemoteServiceCenter,也只有这个档案是在提供服务时一直被占用的,而且它简单到不需要更新就能维持它应有的功能。

       简单的二个介面就解决了长久以来困扰我的问题,好像真的太简单了一点,这样的设计会苦了用户端程式的开发人员,伺服器有什么服务、需要什么参数与回传什么资料完全都不知道,但回想以前VB的开发方式不也是一样?

Dim rds as Object
Set rds = CreateObject("rds.dataspace")
Dim obj as Object
Set obj = rds.CreateObject(物件类别,伺服器路径)
obj.Execute ...

       拿者Object型态的物件也是什么资讯都没有就开始用,有没有错误一切等到runtime才知道,所以也不能说IRemoteServiceCenter.ExecuteService()太过抽象。而像Delphi这种编译时期就会检查型态的语言,当然不能像VB一样拿着TObject型态的物件就开始用,所以必须从伺服器物件汇出一个*.tlb ( COM的Type Library)档给用户端注册取得COM元件的型别,开发Web Service也是同样的道理需要一个参考,所以我们也可以写个用户端物件来描述服务元件,明确列出各项函式参数、回传值型态来叫用抽象的IRemoteServiceCenter.ExecuteService(),例如

public class SomeService{
IRemoteServiceCenter serviceCenter;
const string ServiceName = "SomeService";
public SomeService(string appServerUrl)
{
//从指定的appServerUrl取得IRemoteServiceCenter";this.serviceCenter = (IRemoteServiceCenter)Activator.GetObject(typeof(IRemoteServiceCenter), appServerUrl);
}

public int GetSomeInt()
{
object obj = this.serviceCenter.ExecuteService(ServiceName, "GetSomeInt", null);

if (obj is int)
return (int)obj;

if (obj is Exception)
throw (Exception)obj;

throw new Exception("无法办识的回传值");
}

public void GetSomeString(ref string value1, ref string value2)
{
object obj = this.serviceCenter.ExecuteService(ServiceName, "GetSomeString", new object[] { value1, value2 });

if (obj is string[] && ((string[])obj).Length == 2)
{
value1 = ((string[])obj)[0];
value2 = ((string[])obj)[1];
return;
}

if (obj is Exception)
throw (Exception)obj;

throw new Exception("无法办识的回传值");
}
}
 

       这个物件与伺服器服务物件分属二个不同的档案,一个部署在用户端,一个部署在伺服器,二者互不相关(没有依存性)但要对应的起来,只要做到二个同步更新就不会等到runtime才会发现问题一堆。当然,多写一个这样的物作会增加开发人员的负担,Delphi可以自动汇出COM元件的Type Library,Web Service也可以汇出Web参考,为什么不从IRemoteService汇出这个用户端物件呢?当然可以,为了让开发人员可以用具名的方式定出用户端物件函式参数与IRemoteService参数的关系,我把IRemoteService.Execute的参数与回传值改成IDictionary<string, object>

public interface IRemoteService
{
IDictionary<string, object> Execute(string id, IDictionary<string, object> args);
}

public interface IRemoteServiceCenter{
IDictionary<string, object> ExecuteService(string serviceName, string serviceID, IDictionary<string, object> args);
}


       比照WebService用WebMethodAttribute,我们也可以用自订的Attribute标示要汇出的服务函式,不同于WebMethod,我们的服务函式回传型态都是IDictionary<string, object>,所以需要一个参数做为用户端函式真正的回传型态,服务函式一律用String.Empty做为回传IDictionary<string, object>的索引值

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RemoteServiceFunctionAttribute : Attribute
{
Type returnType;
public RemoteServiceFunctionAttribute(Type returnType)
: base()
{
this.returnType = returnType;
}

public RemoteServiceFunctionAttribute()
: this(typeof(void))
{}

public Type ReturnType { get { return this.returnType; } }
}

[例]

伺服器函式

[RemoteServiceFunction(typeof(int))]
IDictionary<string, object> GetSomeInt(IDictionary<string, object> args)
{
Dictionary<string, object> returnValues = new Dictionary<string, object>();
returnValues.Add(string.Empty, 123);
return returnValues;
}

用户端函式

  

public int GetSomeInt()
{
...
}


       另外我们的服务函式的参数永远只有一个IDictionary<string, object> args,这部份也需要用Attribute来定义用户端函式的参数宣告方式,包括参数的名称、参数型态、传到伺服器时在IDictionary<string, object> args使用的索引值,及从回传结果IDictionary<string, object>取值时使用的索引值(使用具名的方式会比原本的object[]或object容易维护),因为参数有方向性,我刻意依方向性分别宣告一个对应的Attribute来增加程式的可读性(可以一眼看出参数的方向性)

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public abstract class RemoteServiceFunctionParameterAttribute : Attribute{
string name;
Type type;
string inName;
string outName;
protected RemoteServiceFunctionParameterAttribute(string parameterName, Type parameterType, string inArgName, string outArgName)
{
this.name = parameterName;
this.type = parameterType;
this.inName = inArgName;
this.outName = outArgName;
}

public string ParameterName { get { return this.name; } } //宣告用户端函式使用的参数名称

public Type ParameterType { get { return this.type; } } //宣告用户端函式使用的参数型态

public string InArgName { get { return this.inName; } } //传到伺服器使用的名称, null表示不传到伺服器

public string OutArgName { get { return this.outName; } } //从伺服器回传值取值使用的名称, null表示不取值
}

public class InRemoteServiceFunctionParameterAttribute : RemoteServiceFunctionParameterAttribute{
public InRemoteServiceFunctionParameterAttribute(string parameterName, Type parameterType, string inArgName)
: base(parameterName, parameterType, inArgName, null)
{ }
}

public class OutRemoteServiceFunctionParameterAttribute : RemoteServiceFunctionParameterAttribute{
public OutRemoteServiceFunctionParameterAttribute(string parameterName, Type parameterType, string outArgName)
: base(parameterName, parameterType, null, outArgName)
{ }
}

public class RefRemoteServiceFunctionParameterAttribute : RemoteServiceFunctionParameterAttribute{
public RefemoteServiceFunctionParameterAttribute(string parameterName, Type parameterType, string inArgName, string outArgName)
: base(parameterName, parameterType, inArgName, outArgName)
{ }

public RefRemoteServiceFunctionParameterAttribute(string parameterName, Type parameterType, string inOutArgName)
: this(parameterName, parameterType, inOutArgName, inOutArgName)
{ }
}

[例]

伺服器函式

[RemoteServiceFunction]
[RefRemoteServiceFunctionParameter("value1", typeof(string), "arg1")]
[RefRemoteServiceFunctionParameter("value2", typeof(string), "arg2", "CanBeOtherName")]
IDictionary<string, object> GetSomeString(IDictionary<string, object> args)
{
string s1 = (string)args["arg1"];
string s2 = (string)args["arg2"];
//switch s1, s2 then returnDictionary<string, object> returnValues = new Dictionary<string, object>();
returnValues.Add("arg1", s2);
returnValues.Add("CanBeOtherName", s1);
return returnValues;
}

用户端函式

  

public void GetSomeInt(ref string value1, ref string value2)
{
...
}


       所以规则很简单,IRemoteService. Execute(string id, IDictionary<string, object> args)会依id叫用对应的服务函式IDictionary<string, object>[id](IDictionary<string, object> args),只要在这个函式加上[RemoteServiceFunction]标签就会在用户端物件宣告一个对应的函式,用[RemoteServiceFunctionParameter]衍生的三种参数标签来告知用户端函式需要那些参数即可。规则定下来了,如何自动汇出用户端物件呢?这个物件要给用户端程式参考用,所以需要一个实体的档案,可以先做出一个CodeCompileUnit再Compiler成档案,用.Net的CodeDOM写程式虽然又臭又长,但其实一点都不难,只要能融入OO的思考模式就写的出来,以下是自动汇出档案的部份程式:

public static class ServiceClientGenerator{
public static CodeCompileUnit NewCompilerUnit(Type serviceType)
{
CodeCompileUnit unit = new CodeCompileUnit();
// ImportsCodeNamespace ns = new CodeNamespace(serviceType.Namespace + ".Client");
ns.Imports.Add(new CodeNamespaceImport("System"));
ns.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
ns.Imports.Add(new CodeNamespaceImport("System.Text"));
ns.Imports.Add(new CodeNamespaceImport(" Marlon.Wu.Remoting"));
unit.Namespaces.Add(ns);
//建立classCodeTypeDeclaration serviceClass = new CodeTypeDeclaration(serviceType.Name);
serviceClass.TypeAttributes |= TypeAttributes.Public;
ns.Types.Add(serviceClass);
//field - string serviceNameCodeMemberField serviceName = new CodeMemberField(typeof(string), "serviceName");
serviceClass.Members.Add(serviceName);
//field - IRemoteServiceCenter serviceCenter;CodeMemberField serviceCenter = new CodeMemberField(typeof(IRemoteServiceCenter), "serviceCenter");
serviceClass.Members.Add(serviceCenter);
// ConstructorCodeConstructor constructor = new CodeConstructor();
constructor.Attributes = MemberAttributes.Public;
constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "appServerUrl"));
constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "serviceName"));
//this.serviceName = serviceName;
constructor.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "serviceName"), new CodeArgumentReferenceExpression("serviceName")));
//this.serviceCenter = (IRemoteServiceCenter)Activator.GetObject(typeof(IRemoteServiceCenter), appServerUrl);
constructor.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "serviceCenter"), new CodeCastExpression(typeof(IRemoteServiceCenter), new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(Activator)), "GetObject", new CodeExpression[] { new CodeTypeOfExpression(typeof(IRemoteServiceCenter)), new CodeArgumentReferenceExpression("appServerUrl") }))));
serviceClass.Members.Add(constructor);
//ServiceMethodMethodInfo[] methods = serviceType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (MethodInfo method in methods)
{
if (method.ReturnType != typeof(IDictionary<string, object>))
continue;
ParameterInfo[] parameterInfos = method.GetParameters();
if (parameterInfos.Length != 1 || parameterInfos[0].ParameterType != typeof(IDictionary<string, object>))
continue;
//有标示RemoteServiceFunctionAttribute才建立object[] returnValues = method.GetCustomAttributes(typeof(RemoteServiceFunctionAttribute), false);
if (returnValues.Length != 1)
continue;
Type returnValueType = ((RemoteServiceFunctionAttribute)returnValues[0]).ReturnType;
CodeMemberMethod serviceMethod = new CodeMemberMethod();
serviceMethod.Name = method.Name;
serviceMethod.ReturnType = new CodeTypeReference(returnValueType);
serviceMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
serviceClass.Members.Add(serviceMethod);
//参数Dictionary<string, RemoteServiceFunctionParameterAttribute> allParams = new Dictionary<string, RemoteServiceFunctionParameterAttribute>();
Queue<RemoteServiceFunctionParameterAttribute> serviceMethodInParams = new Queue<RemoteServiceFunctionParameterAttribute>();
Queue<RemoteServiceFunctionParameterAttribute> serviceMethodOutParams = new Queue<RemoteServiceFunctionParameterAttribute>();
foreach (RemoteServiceFunctionParameterAttribute paramAttr in method.GetCustomAttributes(typeof(RemoteServiceFunctionParameterAttribute), false))
{
allParams.Add(paramAttr.ParameterName, paramAttr);
CodeParameterDeclarationExpression codeParam = new CodeParameterDeclarationExpression(new CodeTypeReference(paramAttr.ParameterType), paramAttr.ParameterName);
if (paramAttr.InArgName != null)
{
serviceMethodInParams.Enqueue(paramAttr);
if (paramAttr.OutArgName != null)
{
serviceMethodOutParams.Enqueue(paramAttr);
codeParam.Direction = FieldDirection.Ref;
}
}
else if (paramAttr.OutArgName != null)
{
serviceMethodOutParams.Enqueue(paramAttr);
codeParam.Direction = FieldDirection.Out;
}
serviceMethod.Parameters.Add(codeParam);
}
CodeExpression inArgsExpression;
int index;
if (serviceMethodInParams.Count == 0) //不需要传参数{
inArgsExpression = new CodePrimitiveExpression(null);
}
else{
string nameOfInArgs = "inArgs";
index = 0;
while (allParams.ContainsKey(nameOfInArgs))
{
index++;
nameOfInArgs = "inArgs" + index.ToString();
}
//Dictionary<string, object> inArgs = new Dictionary<string,object>();
serviceMethod.Statements.Add(new CodeVariableDeclarationStatement(typeof(Dictionary<string, object>), nameOfInArgs, new CodeObjectCreateExpression(typeof(Dictionary<string, object>), new CodePrimitiveExpression(serviceMethodInParams.Count))));
foreach(RemoteServiceFunctionParameterAttribute inArg in serviceMethodInParams)
serviceMethod.Statements.Add( new CodeMethodInvokeExpression(new CodeVariableReferenceExpression(nameOfInArgs), "Add", new CodeExpression[] { new CodePrimitiveExpression(inArg.InArgName), new CodeArgumentReferenceExpression(inArg.ParameterName) }));
inArgsExpression = new CodeVariableReferenceExpression(nameOfInArgs);
}
//IDictionary<string, object> returnValue = this.serviceCenter.ExecuteService(this.serviceName, "funName", inArgsExpression);
string nameOfReturnValue = "returnValue";
index = 0;
while (allParams.ContainsKey(nameOfReturnValue))
{
index++;
nameOfReturnValue = "returnValue" + index.ToString();
}
CodeMethodInvokeExpression invokeServiceMethod = new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "serviceCenter"), "ExecuteService"), new CodeExpression[] { new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "serviceName"), new CodePrimitiveExpression(method. Name), inArgsExpression });
serviceMethod.Statements.Add(new CodeVariableDeclarationStatement(typeof(IDictionary<string, object>), nameOfReturnValue, invokeServiceMethod));
/*** Return Exception?if (returnValue.ContainsKey(string.Empty) && returnValue[string.Empty] is Exception)throw (Exception)returnValue[string.Empty];***/
serviceMethod.Statements.Add( new CodeConditionStatement( new CodeBinaryOperatorExpression( new CodeMethodInvokeExpression( new CodeVariableReferenceExpression(nameOfReturnValue), "ContainsKey", new CodePrimitiveExpression(string.Empty)), CodeBinaryOperatorType.BooleanAnd, new CodeMethodInvokeExpression( new CodeTypeOfExpression(typeof(Exception)), "IsInstanceOfType", new CodeVariableReferenceExpression(nameOfReturnValue))), new CodeStatement[] { new CodeThrowExceptionStatement( new CodeCastExpression(typeof(Exception), new CodeIndexerExpression(new CodeVariableReferenceExpression(nameOfReturnValue), new CodePrimitiveExpression(String.Empty))))})) ;
//ref or out paramsforeach(RemoteServiceFunctionParameterAttribute outArg in serviceMethodOutParams)
{
serviceMethod.Statements.Add(new CodeAssignStatement( new CodeArgumentReferenceExpression(outArg.ParameterName), new CodeCastExpression(outArg.ParameterType, new CodeIndexerExpression(new CodeVariableReferenceExpression(nameOfReturnValue), new CodePrimitiveExpression(outArg.OutArgName)))));
}
//return valueif (returnValueType != typeof(void))
{
serviceMethod.Statements.Add(new CodeMethodReturnStatement( new CodeCastExpression(returnValueType, new CodeIndexerExpression(new CodeVariableReferenceExpression(nameOfReturnValue), new CodePrimitiveExpression(string.Empty)))));
}
}
//ReferencedAssemblies
unit.ReferencedAssemblies.Add("mscorlib.dll");
unit.ReferencedAssemblies.Add("System.dll");
//compiler前要把定义IRemoteService的相关组件加入参考return unit;
}
}


自动汇出用户端物件测试

伺服器服务物件(只列出服务函式)
public class SomeRemoteService : IRemoteService{
public SomeRemoteService(){ }
  

int refCount;
[RemoteServiceFunction(typeof(int))]
IDictionary<string, object> GetSomeInt(IDictionary<string, object> args)
{
Dictionary<string, object> returnValues = new Dictionary<string, object>();
returnValues.Add(string.Empty, ++refCount);return returnValues;
}

[RemoteServiceFunction]
[RefRemoteServiceFunctionParameter("value1", typeof(string), "arg1")]
[RefRemoteServiceFunctionParameter("value2", typeof(string), "arg2", "CanBeOtherName")]
IDictionary<string, object> GetSomeString(IDictionary<string, object> args)
{
string s1 = (string)args["arg1"];
string s2 = (string)args["arg2"];
//switch s1, s2 then returnDictionary<string, object> returnValues = new Dictionary<string, object>();
returnValues.Add("arg1", s2);
returnValues.Add("CanBeOtherName", s1);
return returnValues;
}
}

public IDictionary<string, object> Execute(string id, IDictionary<string, object> args)
{
...
}
自动产生的用户端物件

//------------------------------------------------ ------------------------------
// <auto-generated>
//这段程式码是由工具产生的。 //执行阶段版本:2.0.50727.1433
//
//对这个档案所做的变更可能会造成错误的行为,而且如果重新产生程式码,//变更将会遗失。 // </auto-generated>
//------------------------------------------------ ------------------------------
namespace Marlon.Wu.Remoting.Client
{
using System;
using System.Collections.Generic;
using System.Text;
using Marlon.Wu.Remoting;

public class SomeRemoteService{
private string serviceName;
private Marlon.Wu.Remoting.IRemoteServiceCenter serviceCenter;

public SomeRemoteService(string appServerUrl, string serviceName)
{
this.serviceName = serviceName;
this.serviceCenter = ((Marlon.Wu.Remoting.IRemoteServiceCenter)(System.Activator.GetObject(typeof(Marlon.Wu.Remoting.IRemoteServiceCenter), appServerUrl)));
}

public int GetSomeInt()
{
System.Collections.Generic.IDictionary<string, object> returnValue = this.serviceCenter.ExecuteService(this.serviceName, "GetSomeInt", null);
if ((returnValue.ContainsKey("") && typeof(System.Exception).IsInstanceOfType(returnValue)))
{
throw ((System.Exception)(returnValue[""]));
}
return ((int)(returnValue[""]));
}

public void GetSomeString(ref string value1, ref string value2)
{
System.Collections.Generic.Dictionary<string, object> inArgs = new System.Collections.Generic.Dictionary<string, object>(2);
inArgs.Add("arg1", value1);
inArgs.Add("arg2", value2);
System.Collections.Generic.IDictionary<string, object> returnValue = this.serviceCenter.ExecuteService(this.serviceName, "GetSomeString", inArgs);
if ((returnValue.ContainsKey("") && typeof(System.Exception).IsInstanceOfType(returnValue)))
{
throw ((System.Exception)(returnValue[""]));
}
value1 = ((string)(returnValue["arg1"]));
value2 = ((string)(returnValue["CanBeOtherName"]));
}
}
}
 

       有了ServiceClientGenerator帮我汇出用户端物件可以让整个开发工作变得非常容易,现在只要维护好每个服务函式的Attribute,每次修改后记得重新用ServiceClientGenerator compiler用户端参考组件档给用户端程式使用,即使伺服器端只提供一个简单抽象的函式,对用户端来说却是一个货真价实的服务元件了。

再来是IRemoteServiceCenter对IRemoteService如何做到动态载入与卸载?传统的dll档不是问题,但.Net不能卸载单一档案,必须卸载整个AppDomain,只要能卸载就不是问题,首先我们可以要求所有实作IRemoteService的物件必须是marshal-by-reference,然后IRemoteServiceCenter也需要一个marshal-by-reference的物件负责动态载入IRemoteService,例如

public class RemoteServiceLoader : MarshalByRefObject{
public IRemoteService LoadService(string asmName, string typeName)
{
return Activator.CreateInstance(asmName, typeName) as IRemoteService;
}
}


IRemoteServiceCenter只要建立一个新的AppDomain,在这个AppDomain建立RemoteServiceLoader物件并取得其参考(proxy),给服务物件的组件及类别名称请RemoteServiceLoader戴入组件并建立服务物件执行个体,不需使用时就把AppDomain卸载释放服务程式,而且AppDomain还提供ShadowCopy的功能,可以把原始档案复制到cache路径执行,这代表什么呢?即使用户端正在使用这只服务程式,因为被载入的是cache路径下的复本,一样可以更新原来的程式,现在连一点点被占用的时间都不会发生,真的是一天24小时都能提供服务,24小时都能更新程式了。

转载自:http://marlon.blog.ithome.com.tw/post/894/17999


本站与之相关的文章:http://www.myfirm.cn/20095/20090517081710634.html


出处:转载

Copyright © 2006-2008 小作坊网 All rights reserved.
备案号:粤ICP备09058104号          电子信箱: jingle_guan#163.com