随着前后端分离的大热,WebApi在项目中的作用也是越来越重要,由于公司的原因我之前一直没有机会参与前后端分离的项目,但WebApi还是要学的呀,因为这东西确实很有用,可单独部署、与前端和App交互都很方便,既然有良好的发展趋势,我们当然应该顺势而为——搞懂WebApi!

从MVC到WebApi,路由机制一直都在其中扮演着重要的角色。

它可以很简单:如果你只需要会用一些简单的路由,如/Home/Index那么你只需要配置一个默认路由就能搞定。

它可以很神秘:你的url可以千变万化,看到一些“无厘头”的url,很难理解它是如何找到匹配的Action,例如/api/Pleasure/1/detail,这样的url可以让你纠结半天。

它可以很深奥:当面试官提问“请简单分析下MVC路由机制的原理”,你可能事先就准备好了答案,然后劈里啪啦一顿(型如:UrlRoutingMoudle—>Routes—>RouteData—>RequestContext—>Controller),你可能回答的很流利,但并不一定理解这些个对象到底是啥意思。):目前为止我还没能理解透,以后会继续努力的直到弄清楚。

一、MVC和WebApi路由机制比较
1、MVC使用的路由
在MVC中,默认路由机制是通过解析url路径来匹配Action。比如:/User/GetList,这个url就表示匹配User控制器下的GetList方法,这是MVC路由的默认解析方式。为什么默认的解析方式是这样子的呢?因为MVC定义了一个默认路由,路由代码放在App_Start文件夹下的RouteConfig.cs中,今后我们如果想要自定义路由规则,那自定义路由的代码也要写在RouteConfig.cs中。

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
url:"{controller}/{action}/{id}"定义了路由解析规则,{controller}是必填参数默认值是Home,{action}是必填参数默认值是Index,{id}表示匹配名称为id的形参,而且是可选参数(方法的参数列表中可以有名为id的形参,也可以没有)。

2、WebApi使用的路由
在WebApi中,默认路由机制是通过解析http请求的类型来匹配Action,也就是说WebApi的默认路由机制不需要指定Action的名称。比如:/api/Pleasure,这个url就表示匹配Pleasure控制器下的[HttpGet]方法,/api是固定必填值,这是WebApi路由的默认解析方式。WebApi的默认解析方式之所以如此,同样也是因为定义了默认路由,路由代码放在App_Start文件夹下的WebApiConfig.cs中,今后我们如果想要自定义路由规则,那自定义路由的代码也要写在WebApiConfig.cs中。

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

}
routeTemplate:"api/{controller}/{id}"定义了路由解析规则,api是固定必填值,{controller}是必填参数无默认值,{id}表示匹配名称为id的形参,而且是可选参数(方法的参数列表中可以有名为id的形参,也可以没有)。

3、MVC和WebApi路由区别汇总
WebApi的默认路由机制通过http请求的类型匹配Action,MVC的默认路由机制通过url匹配Action
WebApi的路由配置文件是WebApiConfig.cs,MVC的路由配置文件是RouteConfig.cs
WebApi的Controller继承自Web.Http.ApiController,MVC的Controller继承自Web.Mvc.Controller
4、示例一
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
}
 
为什么http://localhost:7866/api/Pleasure能匹配到GetOne()方法呢?首先根据路由规则解析出控制器是Pleasure,其次通过浏览器地址栏直接发出的请求都是get请求而Pleasure中只有一个get类型的方法,因此就匹配到了GetOne()。

为什么http://localhost:7866/api/Pleasure/1也能匹配到GetOne()方法呢?因为id是可选形参,即使指定了id的值,也可以访问不含形参id的方法。

5、示例二
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}
}

示例二中http://localhost:7866/api/Pleasure请求的结果与示例一中的结果是一样的,在此不做过多的解释。

示例二中http://localhost:7866/api/Pleasure/1请求到了含参方法,说明如果指定了形参id的值,而且Controller中存在指定请求类型的含参方法,会优先匹配含此形参的方法。若匹配不上含参方法,但参数类型为可选参数,那就再尝试匹配不含此形参的方法。

6、示例三
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}

[HttpGet]
public string GetList()
{
return "value";
}
}

虽然WebApi的默认路由机制不需要指定Action,但是WebApi支持在url中指定Action。由于Restful风格的服务要求请求的url中不能包含Action,因此WebApi不提倡在url中指定Action。

7、示例四
public class PleasureController : ApiController
{
[HttpGet]
public string GetOne(int id,string name,int age)
{
return string.Format("id={0},name={1},age={2}", id, name, age);
}
}

二、WebApi基础
1、默认路由
新建WebApi服务的时候,会自动在WebApiConfig.cs文件里面生成一个默认路由:

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
将MapHttpRoute()方法转到定义可以发现,它有四个重载方法:

下面来看看各个参数的作用:

name:"DefaultApi"表示此路由的名称。注册多个路由时必须保证名称不重复。
routeTemplate:"api/{controller}/{id}"表示路由的匹配规则。api是固定必填值,这个值是可变的如果你把它改成“BalaApi”,那url就应该变成 “http://1.1.1.1:80/BalaApi/***”。{controller}是控制器的占位符,在真实的url中,该部分对应的是具体的控制器名称,这个和MVC一致。{id}是形参id的占位符,id是形参的名字,一般这个参数都会在defaults中设置为可选。
defaults:new { id=RouteParameter.Optional }表示设置id为可选参数,routeTemplate中的{controller}和{id}都可以设置默认值。若defaults改成new { controller="Pleasure", id = RouteParameter.Optional },那么我们请求http://1.1.1.1:80/api这个url仍然能访问到Pleasure控制器中的[HttpGet]方法。
constraints:new{ id = @"\d+" }表示为id添加约束。形参id不能为空而且必须是整数,优先匹配含参方法,也能匹配无参方法(详情请回看上一部分的示例一)。
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}
}

我们如果想在不指定id值的时候仍然能够正常请求到GetOne(),把id的约束改成“constraints: new { id = @"\d*" }”即可,这里就不放截图了,大家可以自己去试试。我们只在路由中对id的值进行了约束,其实我们也可以约束Controller和Action,但一般不常用大家有兴趣可以自己玩一下。

2、自定义路由
上面介绍了许多关于默认路由的内容,除此之外我们还可以自定义路由,比如将WebApiConfig.cs改成下面这样:

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
2.1、自定义匹配到Action的路由
这个自定义路由很好理解,它和MVC中的默认路由大致相同,不同之处是此路由多了个api前缀。
添加了可匹配到Action的路由后,就能解决“第一部分->示例三”中遇到的问题了。
当同时存在多个路由时,一定要注意name不能重复。

public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

我艹,为什么http://localhost:7866/api/Pleasure/GetOne请求失败了,为什么没有匹配上GetOne()方法?
下面我来解释下,DefaultApi定义在ActionApi之前,所以先检查http://localhost:7866/api/Pleasure/GetOne能否与DefaultApi匹配成功,api是固定必填值,Pleasure是controller的值,GetOne是id的值,匹配成功!然后去PleasureController中找[HttpGet]类型而且含有string id形参的方法,当然是没有啦。只找到一个[HttpGet]类型的GetOne(int id),所以错误信息中提示参数id的类型不匹配。

接下来说说解决方法,实际上id是int类型的参数,我们希望GetOne能匹配上ActionApi中的action,而不是DefaultApi中的id,这在默认情况下是做不到的,不过为DefaultApi中的id加上个约束后就能做到了,路由修改后如下:

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

2.2、修改方法的ActionName
WebApi默认GetOne()方法的ActionName就是GetOne,如果我们想要方法名和ActionName不一致,可以通过ActionName特性来修改。

public class PleasureController : ApiController
{
[ActionName("NewGetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

三、WebApi路由执行过程
1、路由原理
有了上面的理论作为基础,我们再来分析WebApi路由机制的原理以及路由匹配的过程。由于WebApi的路由机制和MVC有许多相似性,所以要想理解WebApi的路由机制,就需要搬出那些Asp.Net Routing里面的对象。这个过程有点复杂,我就根据搜罗的资料和自己的理解 ,提一些主要的过程:

a、WebApi服务启动之后,会执行全局配置文件Global.asax.cs的 protected void Application_Start() 方法,其中的 GlobalConfiguration.Configure(WebApiConfig.Register); 会通过委托的方式执行WebApiConfig.cs中的 public static void Register(HttpConfiguration config),将配置的所有路由信息添加到HttpRouteCollection对象中保存起来。这里的HttpRouteCollection对象的实例名是Routes,这个很重要,后面会用到。

b、当我们发送请求到WebApi服务器的时候,比如当我们访问http://localhost:7866/api/Pleasure时,首先请求会被UrlRoutingModule监听组件截获,然后按照定义顺序检查url能匹配上Routes集合中的哪个路由模板(如果都匹配不上,则返回404),最后返回对应的IHttpRoute对象。IHttpRoute对象是url匹配上的Routes集合里面的一个实体。

c、将IHttpRoute对象交给当前的请求上下文对象RequestContext处理,根据IHttpRoute对象中的url匹配到对应的controller,然后根据http请求的类型和参数找到对应的action。这样就能找到请求对应的controller和action了。

这个过程是非常复杂的,为了简化,我只选择了最主要的几个过程。更详细的路由机制可以参考http://www.cnblogs.com/wangiqngpei557/p/3379095.html,这篇文章写得有些深度,感兴趣的朋友可以看下。

2、根据请求的url匹配路由模板
通过上文路由过程的分析,我们知道一个请求过来之后,路由主要经历三个阶段:
1、根据请求的url匹配路由模板
2、找到controller
3、找到action

第一步很简单,主要就是将url与路由模板中配置的routeTemplate进行匹配校验,在此不做过多的说明。

3、找到controller
你如果反编译路由模块的代码,就会发现控制器的选择主要在IHttpControllerSelector接口中的SelectController()方法里面处理。

该方法所需的HttpRequestMessag参数,是由当前请求封装而来,返回HttpControllerDescriptor对象。这个接口默认由DefaultHttpControllerSelector类提供实现。

默认实现的方法里面大致的算法机制是:首先在路由字典中找到实际的控制器名称(比如“Pleasure”),然后在此控制器名称后面加上“Controller”字符串得到控制器的全称“PleasureController”,最后找到WebApi对应的Controller再实例化就得到当前请求的控制器对象。

4、找到action
得到了控制器对象之后,Api引擎通过调用IHttpActionSelector接口中的SelectAction()方法去匹配action。这个过程主要包括:

解析当前http请求,得到请求类型(post、delete、put、get)
如果路由模板配置了{action},则直接去url中找action的名称
解析请求的参数
如果路由模板配置了{action},那么直接去url中找对应的action即可。如果没有配置action,则会首先匹配请求类型(post/delete/put/get),然后匹配请求参数,才能找到对应的action。

5、设置方法同时支持多种http请求类型
WebApi提供了AcceptVerbs,应用这一特性可以使方法同时支持多种请求类型。这一特性在实际中使用不是太多,大家了解即可。

[AcceptVerbs("get","post")]
public string GetList()
{
return "GetList()->value";
}
四、WebApi特性路由
如果http请求的类型相同(比如都是get请求),并且请求的参数也相同。这个时候似乎就有点不太好办了,这种情况在实际项目中还是比较多的,比如:

public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
 当然这个问题可以通过添加匹配到Action的路由来解决,不过这就不符合Restful风格了,所有我们不提倡这种写法。除此之外,利用特性路由也能解决上述问题,下面说说特性路由的东西。

1、启动特性路由
public static void Register(HttpConfiguration config)
{
//启动特性路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
一般情况下通过新版本的VS2017创建WebApi项目时,这句话默认已经存在。

2、最简单的无参特性路由

2.1、为GetOne()方法添加Route特性(特性路由没参数,GetOne()方法也没参数)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

结论:

为GetOne()添加了特性路由后,无法通过WebApi默认路由机制(不指定Action)访问GetOne()
为GetOne()添加了特性路由后,无法通过指定ActionName的方式访问GetOne()
为GetOne()添加了特性路由后,只能通过特性路由的路径访问GetOne()
2.2、为GetOne()方法添加Route特性(特性路由没参数,GetOne()方法有参数)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

特性路由不含参数时的结论:

方法没参数时,只要url满足特性路由的定义就能访问到方法
方法有参数时,必须url满足特性路由的规则而且指定了方法所需的参数值才能访问到方法
3、含参特性路由
3.1、特性路由有参数,方法没参数
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

3.2、特性路由有参数,方法有参数
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string address)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

特性路由含参数时的结论:

方法没参数时,只要url满足特性路由的定义就能访问到方法
方法有参数时,必须url满足特性路由的规则而且指定了方法所需的参数值才能访问到方法
3.3、特性路由有参数,参数在最后而且有默认值
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

3.4、特性路由有参数,参数在中间而且有默认值
public class PleasureController : ApiController
{
[Route("Pleasure/{age:int=16}/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

4、路由前缀
在正式项目中,同一个控制器中所有的action的特性路由最好有一个相同的前缀,这并非是必须的,但能增加url的可读性。一般的做法是在控制器上使用[RoutePrefix]特性来标识。当我们使用特性路由来访问时,前边必须加上定义的前缀。

[RoutePrefix("api/prefix")]
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

————————————————
版权声明:本文为CSDN博主「changuncle」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaouncle/article/details/83869952

WebApi路由机制详解的更多相关文章

  1. SPA路由机制详解(看不懂不要钱~~)

    前言 总所周知,随着前端应用的业务功能起来越复杂,用户对于使用体验的要求越来越高,单面(SPA)成为前端应用的主流形式.而大型单页应用最显著特点之一就是采用的前端路由跳转子页面系统,通过改变页面的UR ...

  2. typecho路由机制详解

    本文介绍的是typecho的路由机制,引自 不烦恼路由机制是typecho的核心,有很多功能都是基于路由功能设计的,理解并熟悉TE的路由机制将非常有助于插件的开发. 完整的路由表如下: array ( ...

  3. C#进阶系列——WebApi 路由机制剖析:你准备好了吗?

    前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...

  4. C#进阶系列——WebApi 路由机制剖析:你准备好了吗? 转载https://www.cnblogs.com/landeanfen/p/5501490.html

    阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...

  5. WebApi 路由机制剖析

    阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...

  6. 从mixin到new和prototype:Javascript原型机制详解

    从mixin到new和prototype:Javascript原型机制详解   这是一篇markdown格式的文章,更好的阅读体验请访问我的github,移动端请访问我的博客 继承是为了实现方法的复用 ...

  7. 浏览器 HTTP 协议缓存机制详解

    最近在准备优化日志请求时遇到了一些令人疑惑的问题,比如为什么响应头里出现了两个 cache control.为什么明明设置了 no cache 却还是发请求,为什么多次访问时有时请求里带了 etag, ...

  8. JVM的垃圾回收机制详解和调优

    JVM的垃圾回收机制详解和调优 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都 ...

  9. ThreadPoolExecutor运转机制详解

    ThreadPoolExecutor运转机制详解 - 走向架构师之路 - 博客频道 - CSDN.NET 最近发现几起对ThreadPoolExecutor的误用,其中包括自己,发现都是因为没有仔细看 ...

随机推荐

  1. Mysql DBA

    1 mysqldump: Error 2020: Got packet bigger than 'max_allowed_packet' bytes when dumping table `tb_co ...

  2. 在Linux下安装PyEmu

    git clone https://github.com/OpenRCE/pydbg.git git clone https://github.com/OpenRCE/paimei.git libda ...

  3. C++的模板

    1. 模板形参表 模板形参表,里面可以是typename T/ class T这种形式的,代表里面被泛化的是一种类型: 也可以使用Type value这种形式的,代表里面被泛化的是一个某种类型的值. ...

  4. tomcat服务器和HTTP协议

    tomcat:一个服务器的服务器软件,发布资源要用的 服务器组成: 1.服务器硬件 2.服务器软件 3.项目(一堆资源的集合) 4.资源tomcat本身是一个java程序,必须依赖jre运行eclip ...

  5. Linux基本知识点

    压缩和解压类 7.8.1 gzip/gunzip 压缩 1.基本语法 gzip 文件 (功能描述:压缩文件,只能将文件压缩为*.gz文件) gunzip 文件.gz (功能描述:解压缩文件命令) 2. ...

  6. 使用Python将字符串转换为格式化的日期时间字符串

    我正在尝试将字符串“20091229050936”转换为“2009年12月29日(UTC)” >>>import time >>>s = time.strptime ...

  7. Kettle使用kettle.properties

    kettle.properties 是一个变量文件,这个文件我使用的最多的地方是保存 “数据库连接” 用户名和密码. 如果不用这个文件,那么使用“数据库连接”时,需要硬编码写到文件里. 有一天dba告 ...

  8. style优先级

    不同级别 在属性后面使用 !important 会覆盖页面内任何位置定义的元素样式. 作为style属性写在元素内的样式 id选择器 类选择器 标签选择器 通配符选择器 浏览器自定义或继承       ...

  9. PROJECT | 四则运算UI设计 - 项目总结

    [项目Github地址] https://github.com/oTPo/hw2 [项目规划] PSP表格 事项 预计时间(min) 实际花费时间(min) 需求分析 60 60 开发流程分析 30 ...

  10. 基于nginx+tomcat部署商城系统并连接数据库

    需三台服务器nginx 192.168.200.111tomcat 192.168.200.112tomcat 192.168.200.113 192.168.200.111[root@localho ...