[转]Creating an OData v3 Endpoint with Web API 2
by Mike Wasson+
The Open Data Protocol (OData) is a data access protocol for the web. OData provides a uniform way to structure data, query the data, and manipulate the data set through CRUD operations (create, read, update, and delete). OData supports both AtomPub (XML) and JSON formats. OData also defines a way to expose metadata about the data. Clients can use the metadata to discover the type information and relationships for the data set.+
ASP.NET Web API makes it easy to create an OData endpoint for a data set. You can control exactly which OData operations the endpoint supports. You can host multiple OData endpoints, alongside non-OData endpoints. You have full control over your data model, back-end business logic, and data layer.+
Software versions used in the tutorial
- Visual Studio 2013
- Web API 2
- OData Version 3
- Entity Framework 6
- Fiddler Web Debugging Proxy (Optional)
Web API OData support was added in ASP.NET and Web Tools 2012.2 Update. However, this tutorial uses scaffolding that was added in Visual Studio 2013.+
In this tutorial, you will create a simple OData endpoint that clients can query. You will also create a C# client for the endpoint. After you complete this tutorial, the next set of tutorials show how to add more functionality, including entity relations, actions, and $expand/$select.+
Create the Visual Studio Project
In this tutorial, you will create an OData endpoint that supports basic CRUD operations. The endpoint will expose a single resource, a list of products. Later tutorials will add more features.+
Start Visual Studio and select New Project from the Start page. Or, from the File menu, select New and then Project.+
In the Templates pane, select Installed Templates and expand the Visual C# node. Under Visual C#, select Web. Select the ASP.NET Web Application template.+
In the New ASP.NET Project dialog, select the Empty template. Under "Add folders and core references for...", check Web API. Click OK.+
Add an Entity Model
A model is an object that represents the data in your application. For this tutorial, we need a model that represents a product. The model corresponds to our OData entity type.+
In Solution Explorer, right-click the Models folder. From the context menu, select Add then select Class.+
In the Add New Item dialog, name the class "Product".+
Note
By convention, model classes are placed in the Models folder. You don't have to follow this convention in your own projects, but we'll use it for this tutorial.+
In the Product.cs file, add the following class definition:+
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
The ID property will be the entity key. Clients can query products by ID. This field would also be the primary key in the back-end database.+
Build the project now. In the next step, we'll use some Visual Studio scaffolding that uses reflection to find the Product type.+
Add an OData Controller
A controller is a class that handles HTTP requests. You define a separate controller for each entity set in you OData service. In this tutorial, we'll create a single controller.+
In Solution Explorer, right-click the the Controllers folder. Select Add and then select Controller.+
In the Add Scaffold dialog, select "Web API 2 OData Controller with actions, using Entity Framework".+
In the Add Controller dialog, name the controller "ProductsController". Select the "Use async controller actions" checkbox. In the Model drop-down list, select the Product class.+
Click the New data context... button. Leave the default name for the data context type, and click Add.+
Click Add in the Add Controller dialog to add the controller.+
Note: If you get an error message that says "There was an error getting the type...", make sure that you built the Visual Studio project after you added the Product class. The scaffolding uses reflection to find the class.+
The scaffolding adds two code files to the project:+
- Products.cs defines the Web API controller that implements the OData endpoint.
- ProductServiceContext.cs provides methods to query the underlying database, using Entity Framework.
Add the EDM and Route
In Solution Explorer, expand the App_Start folder and open the file named WebApiConfig.cs. This class holds configuration code for Web API. Replace this code with the following:+
using ProductService.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
namespace ProductService
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
}
This code does two things:+
- Creates an Entity Data Model (EDM) for the OData endpoint.
- Adds a route for the endpoint.
An EDM is an abstract model of the data. The EDM is used to create the metadata document and define the URIs for the service. The ODataConventionModelBuilder creates an EDM by using a set of default naming conventions EDM. This approach requires the least code. If you want more control over the EDM, you can use the ODataModelBuilder class to create the EDM by adding properties, keys, and navigation properties explicitly.+
The EntitySet method adds an entity set to the EDM:+
modelBuilder.EntitySet<Product>("Products");
The string "Products" defines the name of the entity set. The name of the controller must match the name of the entity set. In this tutorial, the entity set is named "Products" and the controller is named ProductsController. If you named the entity set "ProductSet", you would name the controller ProductSetController. Note that an endpoint can have multiple entity sets. Call EntitySet<T> for each entity set, and then define a corresponding controller.+
The MapODataRoute method adds a route for the OData endpoint.+
config.Routes.MapODataRoute("ODataRoute", "odata", model);
The first parameter is a friendly name for the route. Clients of your service do not see this name. The second parameter is the URI prefix for the endpoint. Given this code, the URI for the Products entity set is http://hostname/odata/Products. Your application can have more than one OData endpoint. For each endpoint, call MapODataRoute and provide a unique route name and a unique URI prefix.+
Seed the Database (Optional)
In this step, you will use Entity Framework to seed the database with some test data. This step is optional, but it lets you test out your OData endpoint right away.+
From the Tools menu, select Library Package Manager, then select Package Manager Console. In the Package Manager Console window, enter the following command:+
Enable-Migrations
This adds a folder named Migrations and a code file named Configuration.cs.+
Open this file and add the following code to the Configuration.Seed method.+
protected override void Seed(ProductService.Models.ProductServiceContext context)
{
// New code
context.Products.AddOrUpdate(new Product[] {
new Product() { ID = 1, Name = "Hat", Price = 15, Category = "Apparel" },
new Product() { ID = 2, Name = "Socks", Price = 5, Category = "Apparel" },
new Product() { ID = 3, Name = "Scarf", Price = 12, Category = "Apparel" },
new Product() { ID = 4, Name = "Yo-yo", Price = 4.95M, Category = "Toys" },
new Product() { ID = 5, Name = "Puzzle", Price = 8, Category = "Toys" },
});
}
In the Package Manager Console Window, enter the following commands:+
Add-Migration Initial
Update-Database
These commands generate code that creates the database, and then executes that code.+
Exploring the OData Endpoint
In this section, we'll use the Fiddler Web Debugging Proxy to send requests to the endpoint and examine the response messages. This will help you to understand the capabilities of an OData endpoint.+
In Visual Studio, press F5 to start debugging. By default, Visual Studio opens your browser to http://localhost:*port*, where port is the port number configured in the project settings.+
You can change the port number in the project settings. In Solution Explorer, right-click the project and select Properties. In the properties window, select Web. Enter the port number under Project Url.+
Service Document
The service document contains a list of the entity sets for the OData endpoint. To get the service document, send a GET request to the root URI of the service.+
Using Fiddler, enter the following URI in the Composer tab: http://localhost:port/odata/, where port is the port number.+
Click the Execute button. Fiddler sends an HTTP GET request to your application. You should see the response in the Web Sessions list. If everything is working, the status code will be 200.+
Double-click the response in the Web Sessions list to see the details of the response message in the Inspectors tab.+
The raw HTTP response message should look similar to the following:+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 17:51:01 GMT
Content-Length: 364
<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:60868/odata"
xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
<workspace>
<atom:title type="text">Default</atom:title>
<collection href="Products">
<atom:title type="text">Products</atom:title>
</collection>
</workspace>
</service></pre>
By default, Web API returns the service document in AtomPub format. To request JSON, add the following header to the HTTP request:+
Accept: application/json+
Now the HTTP response contains a JSON payload:+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 22:59:28 GMT
Content-Length: 136
{
"odata.metadata":"http://localhost:60868/odata/$metadata","value":[
{
"name":"Products","url":"Products"
}
]
}
Service Metadata Document
The service metadata document describes the data model of the service, using an XML language called the Conceptual Schema Definition Language (CSDL). The metadata document shows the structure of the data in the service, and can be used to generate client code.+
To get the metadata document, send a GET request to http://localhost:port/odata/$metadata. Here is the metadata for the endpoint shown in this tutorial.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:05:52 GMT
Content-Length: 1086
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="ProductService.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Price" Type="Edm.Decimal" Nullable="false" />
<Property Name="Category" Type="Edm.String" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Products" EntityType="ProductService.Models.Product" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Entity Set
To get the Products entity set, send a GET request to http://localhost:port/odata/Products.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:01:31 GMT
Content-Length: 459
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products","value":[
{
"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
},{
"ID":2,"Name":"Socks","Price":"5.00","Category":"Apparel"
},{
"ID":3,"Name":"Scarf","Price":"12.00","Category":"Apparel"
},{
"ID":4,"Name":"Yo-yo","Price":"4.95","Category":"Toys"
},{
"ID":5,"Name":"Puzzle","Price":"8.00","Category":"Toys"
}
]
}
Entity
To get an individual product, send a GET request to http://localhost:port/odata/Products(1), where "1" is the product ID.+
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:04:29 GMT
Content-Length: 140
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element","ID":1,
"Name":"Hat","Price":"15.00","Category":"Apparel"
}
OData Serialization Formats
OData supports several serialization formats:+
- Atom Pub (XML)
- JSON "light" (introduced in OData v3)
- JSON "verbose" (OData v2)
By default, Web API uses AtomPubJSON "light" format. +
To get AtomPub format, set the Accept header to "application/atom+xml". Here is an example response body:+
<?xml version="1.0" encoding="utf-8"?>
<entry xml:base="http://localhost:60868/odata" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>http://localhost:60868/odata/Products(1)</id>
<category term="ProductService.Models.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="http://localhost:60868/odata/Products(1)" />
<link rel="self" href="http://localhost:60868/odata/Products(1)" />
<title />
<updated>2013-09-23T23:42:11Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:ID m:type="Edm.Int32">1</d:ID>
<d:Name>Hat</d:Name>
<d:Price m:type="Edm.Decimal">15.00</d:Price>
<d:Category>Apparel</d:Category>
</m:properties>
</content>
</entry>
You can see one obvious disadvantage of the Atom format: It's a lot more verbose than the JSON light. However, if you have a client that understands AtomPub, the client might prefer that format over JSON.+
Here is the JSON light version of the same entity:+
{
"odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element",
"ID":1,
"Name":"Hat",
"Price":"15.00",
"Category":"Apparel"
}
The JSON light format was introduced in version 3 of the OData protocol. For backward compatibility, a client can request the older "verbose" JSON format. To request verbose JSON, set the Accept header to application/json;odata=verbose. Here is the verbose version:+
{
"d":{
"__metadata":{
"id":"http://localhost:18285/odata/Products(1)",
"uri":"http://localhost:18285/odata/Products(1)",
"type":"ProductService.Models.Product"
},"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
}
}
This format conveys more metadata in the response body, which can add considerable overhead over an entire session. Also, it adds a level of indirection by wrapping the object in a property named "d".+
Next Steps
using Microsoft.Data.OData;
using Microsoft.Data.OData.Query;
using ProductService.Models;
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.OData;
using System.Web.Http.OData.Routing;
using System.Web.Http.Routing; namespace ProductService.Controllers
{
public class ProductsController : ODataController
{
private ProductServiceContext db = new ProductServiceContext(); // GET odata/Products
[Queryable]
public IQueryable<Product> GetProducts()
{
return db.Products;
} // GET odata/Products(5)
[Queryable]
public SingleResult<Product> GetProduct([FromODataUri] int key)
{
return SingleResult.Create(db.Products.Where(product => product.ID == key));
} // PUT odata/Products(5)
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} if (key != product.ID)
{
return BadRequest();
} db.Entry(product).State = EntityState.Modified; try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(key))
{
return NotFound();
}
else
{
throw;
}
} return Updated(product);
} // POST odata/Products
public async Task<IHttpActionResult> Post(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} db.Products.Add(product);
await db.SaveChangesAsync(); return Created(product);
} // PATCH odata/Products(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> patch)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} patch.Patch(product); try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(key))
{
return NotFound();
}
else
{
throw;
}
} return Updated(product);
} // DELETE odata/Products(5)
public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} db.Products.Remove(product);
await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent);
} // GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
Product product = db.Products.FirstOrDefault(p => p.ID == key);
if (product == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return product.Supplier;
} protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
} private bool ProductExists(int key)
{
return db.Products.Count(e => e.ID == key) > ;
} // The next methods support entity relations.
// For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} switch (navigationProperty)
{
case "Supplier":
string supplierKey = GetKeyFromLinkUri<string>(link);
Supplier supplier = await db.Suppliers.FindAsync(supplierKey);
if (supplier == null)
{
return NotFound();
}
product.Supplier = supplier;
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent); default:
return NotFound();
}
} public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty)
{
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} switch (navigationProperty)
{
case "Supplier":
product.Supplier = null;
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent); default:
return NotFound(); }
} // Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
TKey key = default(TKey); // Get the route that was used for this request.
IHttpRoute route = Request.GetRouteData().Route; // Create an equivalent self-hosted route.
IHttpRoute newRoute = new HttpRoute(route.RouteTemplate,
new HttpRouteValueDictionary(route.Defaults),
new HttpRouteValueDictionary(route.Constraints),
new HttpRouteValueDictionary(route.DataTokens), route.Handler); // Create a fake GET request for the link URI.
var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link); // Send this request through the routing process.
var routeData = newRoute.GetRouteData(
Request.GetConfiguration().VirtualPathRoot, tmpRequest); // If the GET request matches the route, use the path segments to find the key.
if (routeData != null)
{
ODataPath path = tmpRequest.GetODataPath();
var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
if (segment != null)
{
// Convert the segment into the key type.
key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
segment.Value, ODataVersion.V3);
}
}
return key;
} // The next method implements an OData action.
// For more information, see http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-actions // POST /odata/Products(5)/RateProduct
[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
} int rating = (int)parameters["Rating"]; Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
} product.Ratings.Add(new ProductRating() { Rating = rating });
db.SaveChanges(); double average = product.Ratings.Average(x => x.Rating); return Ok(average);
} }
}
[转]Creating an OData v3 Endpoint with Web API 2的更多相关文章
- AngularJS使用OData请求ASP.NET Web API资源的思路
本篇整理AngularJS使用OData请求ASP.NET Web API资源的思路. 首先给ASP.NET Web API插上OData的翅膀,通过NuGet安装OData. 然后,给control ...
- Creating Help Pages for ASP.NET Web API -摘自网络
When you create a web API, it is often useful to create a help page, so that other developers will k ...
- OData查询ASP.NET Web API全攻略
本篇使用ASP.NET Web API来体验OData各种query. 首先是本篇即将用到的Model.使用的OData版本是4.0. public class Customer { public i ...
- Create an OData v4 Endpoint Using ASP.NET Web API 2.2(使用ASP.NET Web API 2.2创建OData v4端点)
开放数据协议Open Data Protocol(OData)是web的一种数据存取协议,OData通过设置CRUD操作(Create创建.Read读取.Update更新,Delete删除)提供一种统 ...
- 对一个前端AngularJS,后端OData,ASP.NET Web API案例的理解
依然chsakell,他写了一篇前端AngularJS,后端OData,ASP.NET Web API的Demo,关于OData在ASP.NET Web API中的正删改查没有什么特别之处,但在前端调 ...
- Web API 2
Asp.Net Web API 2 官网菜鸟学习系列导航[持续更新中] 前言 本来一直参见于微软官网进行学习的, 官网网址http://www.asp.net/web-api.出于自己想锻炼一下学 ...
- 杂项:ASP.NET Web API
ylbtech-杂项:ASP.NET Web API ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动设备)的 HTTP 服务. ASP.NET Web A ...
- ASP.NET Web API系列教程目录
ASP.NET Web API系列教程目录 Introduction:What's This New Web API?引子:新的Web API是什么? Chapter 1: Getting Start ...
- ASP.NET Web API系列教程(目录)(转)
注:微软随ASP.NET MVC 4一起还发布了一个框架,叫做ASP.NET Web API.这是一个用来在.NET平台上建立HTTP服务的Web API框架,是微软的又一项令人振奋的技术.目前,国内 ...
随机推荐
- C# SM加密
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Org.Boun ...
- StarUML3.0选择不同类型图和导出
StarUML(简称SU),是一种创建UML类图,生成类图和其他类型的统一建模语言(UML)图表的工具. 可绘制9款UML图:用例图.类图.序列图.状态图.活动图.通信图.构件图.部署图以及复合结构图 ...
- 总结目前为止学到的关键字(break,continue,private,static,this,super,final,abstract)
1.控制跳转语句:break(结束) 使用的场景: a.循环当中 b.switch break关键字需要注意的问题: 1.break关键字只能用于循环和switch语句当中,其本质就是结束整段语句的意 ...
- char、varchar、nchar、nvarchar、text的区别
char.varchar.nchar.nvarchar.text的区别 1.有var前缀的,表示是实际存储空间是变长的,varchar,nvarchar 所谓定长就是长度固定的,当输入的数据长度没有达 ...
- 【cocos2d-x 手游研发小技巧(5)获取网络图片缓存并展示】
今天是年前最后一天上班了,最后一天上班,祝大家马上有各种东西,最后一天也给写一点干货,就是获取网络图片: 经过自己简单封装了一下,实现了获取网络图片,按照比例展示出来,实现方法是cocos2dx - ...
- Android 多线程基础
需要注意几个概念:Runnable,Thread,Handler. 1. Runnable只是一个接口,里面包含run()函数.所以Runnable本身不会开启线程. 2. Thread实现Runna ...
- python中的内置函数(一), lambda, filter, map
https://www.processon.com/view/link/5c10da0ce4b099ae3e137bf6 1.内置函数 内置函数就是python中提供的,可以直接拿来用的函数,比如pr ...
- iOS开发网络篇—GET请求和POST请求的说明与比较
1.GET请求和POST请求简单说明 1.1 创建GET请求 // 1.设置请求路径 NSString *urlStr = [NSString stringWithFormat:@"http ...
- JavaScript求数组Array的并集(javascript面试常见题目)
var Utils = { joinArray:function(source,target){ for(var i = 0;i<source.length;i++){ var oa = sou ...
- 高可用群集HA介绍与LVS+keepalived高可用群集
一.Keepalived介绍 通常使用keepalived技术配合LVS对director和存储进行双机热备,防止单点故障,keepalived专为LVS和HA设计的一款健康检查工具,但演变为后来不仅 ...















