前言
傳統的Asmx服務,由於遵循SOAP協議,所以返回內容以xml方式組織。並且客戶端需要添加服務端引用才能使用(雖然看到網絡上已經提供了這方面的Dynamic Proxy,但是沒有這種方式簡便),所以給開發和部署帶來了不小的麻煩。並且當服務過多的時候,生成的引用文件會很大,之前項目的一個引用文件光引用代碼都有5000多行,全部在一個類中。確實不方便維護。
基於以上幾點,就特別研究了一下基於Restful的服務開發,當時手頭有兩種框架,一個是WCF Restful Service,另一個是Asp.net Web API。由於對WCF比較熟悉一些,所以就選擇了前者。
Restful Service及其相關
說到Restful Service,不得不提到其中的Rest這個關鍵字。它是用於創建分布式超文本媒體的一種架構方式,我們可以通過標准的HTTP(GET,POST,PUT,DELETE)操作來構建基於面向資源的軟件架構方式(Resource-Oriented Architecture (ROA))。它是獨立於任何技術或者平台的,所以人們經常將符合這種操作規范的服務稱為“RESTful services”。因為WCF能夠構建符合這種規范的服務,所以我們經常稱之為 WCF Restful Services。
由於傳統的WCF Service可以使用tcp,net.msmq,http等協議進行數據交換,並且采用了RPC(Remote Procedure Call)的工作方式,客戶端需要添加對服務端的引用才能完成。但是WCF Restful Service完全使用Http協議來進行,並且無需添加客戶端引用,所以方便很多。
服務端開發一瞥
下面以圖書館的例子來做具體的說明。
打開VS2010,新建一個WCF REST Service Application項目,然后在項目中,添加一個BookService.cs用於處理邏輯操作,再添加一個BookEntity.cs用於提供實體類。
打開Global.asax,可以看到如下代碼:
1: void Application_Start(object sender, EventArgs e)
2: {
3: RegisterRoutes();
4: }
5:
6: private void RegisterRoutes()
7: {
8: RouteTable.Routes.Add(new ServiceRoute("BookService", new WebServiceHostFactory(), typeof(BookService)));
9: }
其中RegisterRoutes是設定服務啟動的入口點的。
然后是BookEntity實體類的組織方式:
1: public class BookEntity
2: {
3: public int BookID { get; set; }
4:
5: public string BookName { get; set; }
6:
7: public decimal BookPrice { get; set; }
8:
9: public string BookPublish { get; set; }
10: }
這里我就不用多說了,實體類包含圖書序號,圖書名稱,圖書價格,出版單位四個屬性。
然后就是我們的核心內容:
1: [ServiceContract]
2: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
3: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
4: public class BookService
5: {
6: public BookService()
7: {
8: bookList = new List<BookEntity>();
9: BookEntity book = new BookEntity();
10: book.BookID = 1;
11: book.BookName = "大話設計模式";
12: book.BookPrice = (decimal)45.2;
13: book.BookPublish = "中國郵電出版社";
14: bookList.Add(book);
15:
16: BookEntity book1 = new BookEntity();
17: book1.BookID = 2;
18: book1.BookName = "測試用例";
19: book1.BookPrice = (decimal)21.0;
20: book1.BookPublish = "清華大學出版社";
21: bookList.Add(book1);
22:
23: BookEntity book2 = new BookEntity();
24: book2.BookID = 3;
25: book2.BookName = "Rework";
26: book2.BookPrice = (decimal)15.4;
27: book2.BookPublish = "Wrox pulishment";
28: bookList.Add(book2);
29: }
30:
31: private static List<BookEntity> bookList;
32:
33: [WebInvoke(Method = "GET"
34: , ResponseFormat = WebMessageFormat.Json
35: , BodyStyle = WebMessageBodyStyle.Bare //不需要任何修飾,否則生成的json無法解析
36: , UriTemplate = "/?bookID={bookID}")] //只接收string類型,如果是其他類型,需要按照 /?para={parameter}的方式來組織。
37: public BookEntity Get(int bookID)
38: {
39: return bookList.Where(p => p.BookID == bookID).FirstOrDefault();
40: }
41:
42: [WebInvoke(Method = "GET"
43: , ResponseFormat = WebMessageFormat.Json
44: , BodyStyle = WebMessageBodyStyle.Bare
45: , UriTemplate = "/")]
46: public List<BookEntity> GetALL()
47: {
48: return bookList;
49: }
50:
51: [WebInvoke(Method = "POST"
52: , ResponseFormat = WebMessageFormat.Json
53: , BodyStyle = WebMessageBodyStyle.Bare
54: , UriTemplate = "/")]
55: public bool Update(BookEntity book)
56: {
57: BookEntity query = (from p in bookList where p.BookID == book.BookID select p).FirstOrDefault();
58: bookList.Remove(query);
59: bookList.Add(book);
60: return true;
61: }
62:
63: [WebInvoke(Method = "PUT"
64: , ResponseFormat = WebMessageFormat.Json
65: , BodyStyle = WebMessageBodyStyle.Bare
66: , UriTemplate = "/")]
67: public bool Add(BookEntity book)
68: {
69: bookList.Add(book);
70: return true;
71: }
72:
73: [WebInvoke(Method = "DELETE"
74: , ResponseFormat = WebMessageFormat.Json
75: , BodyStyle = WebMessageBodyStyle.Bare
76: , UriTemplate = "/")]
77: public bool Delete(BookEntity book)
78: {
79: BookEntity bookCurrent = (from p in bookList where p.BookID == book.BookID select p).FirstOrDefault();
80: return bookList.Remove(bookCurrent);
81: }
82: }
其中,Method 方法主要是表明可以接受客戶端的請求類型,這里有四種:GET,POST,PUT,DELETE,其中GET為請求數據,POST為更新數據,PUT為新增數據,DELETE代表着刪除數據。
然后ResponseFormat 則代表着返回的數據組織,如果是Json則表明客戶端會接收到Json數據,如果是XML則表明客戶端會接收到XML組織的數據。BodyStyle 代表返回數據的包裝對象,如果是Bare則表明數據無任何包裝,原生數據返回;如果是Wrapped則表明數據會在最外層包裝一個當前函數名稱加上Result的套。比如對於Delete對象,則會返回 DeleteResult:{******},會造成DataContractJsonSerializer無法進行反序列化。
UriTemplate 主要用於指定操作的URI路徑,只要用戶輸入了合法路徑並采用了正確的請求方式,就會觸發該函數。
最后說到的就是URI后面跟的參數的問題,由於函數只能接受string類型的,所以如果傳入參數是string類型,則可以使用UriTemplate = "{bookID}"的路徑,反之,則需要加上/?param1={paramname}的方式,比如我代碼中使用的是:UriTemplate = "/?bookID={bookID}"。
當一切都弄好以后,讓我們運行一下,訪問如下路徑,就可以得到結果:
http://localhost:45345/BookService/
得到的結果如下:
[{"BookID":1,"BookName":"大話設計模式","BookPrice":45.2,"BookPublish":"中國郵電出版社"},{"BookID":2,"BookName":"測試用例","BookPrice":21,"BookPublish":"清華大學出版社"},{"BookID":3,"BookName":"Rework","BookPrice":15.4,"BookPublish":"Wrox pulishment"}]
如果訪問http://localhost:45345/BookService/?bookID=1,則會得到如下的結果:
{"BookID":1,"BookName":"大話設計模式","BookPrice":45.2,"BookPublish":"中國郵電出版社"}
客戶端開發一瞥
初步測試成功后,讓我們來進行一下全面的測試。
首先,在項目中,我們新建一個Asp.net WebForm Application,用於做測試工作。
然后,在Default.aspx.cs中,針對GET操作,我們添加如下代碼:
1: private void GetBookByID(string id)
2: {
3: WebClient proxy = new WebClient();
4: string serviceURL = string.Empty;
5: DataContractJsonSerializer obj ;
6: if (string.IsNullOrEmpty(id))
7: {
8: serviceURL = string.Format("http://localhost:45345/BookService/");
9: obj = new DataContractJsonSerializer(typeof(List<BookEntity>));
10: }
11: else
12: {
13: serviceURL = string.Format("http://localhost:45345/BookService/?bookID=" + id);
14: obj = new DataContractJsonSerializer(typeof(BookEntity));
15: }
16: byte[] data = proxy.DownloadData(serviceURL);
17: Stream stream = new MemoryStream(data);
18: var result = obj.ReadObject(stream);
19: List<BookEntity> list=new List<BookEntity>();
20: if (result is BookEntity)
21: list.Add(result as BookEntity);
22: else if (result is List<BookEntity>)
23: list = result as List<BookEntity>;
24: GridView1.DataSource = list;
25: GridView1.DataBind();
26: }
在以上代碼中,DataContractJsonSerializer 是WCF提供的一個序列化類,用於將對象序列化或者反序列化。
寫好之后,我們點擊界面按鈕,出現了以下的結果:
針對PUT操作,也就是添加操作,我們添加如下代碼:
1: BookEntity bookEntity = new BookEntity();
2: bookEntity.BookID = Int32.Parse(txtBookID.Text);
3: bookEntity.BookName = txtBookName.Text;
4: bookEntity.BookPrice = decimal.Parse(txtBookPrice.Text);
5: bookEntity.BookPublish = txtBookPublish.Text;
6:
7: DataContractJsonSerializer obj = new DataContractJsonSerializer(typeof(BookEntity));
8: MemoryStream ms = new MemoryStream();
9: obj.WriteObject(ms, bookEntity);
10: byte[] byteSend = ms.ToArray();
11: ms.Close();
12:
13: string serviceURL = string.Format("http://localhost:45345/BookService");
14:
15: WebClient test = new WebClient();
16: test.Headers.Add("Content-Type", "application/json");
17: test.Headers.Add("ContentLength", byteSend.Length.ToString());
18:
19:
20: byte[] responseData = test.UploadData(serviceURL, "PUT", byteSend);
21:
22: string result = Encoding.GetEncoding("UTF-8").GetString(responseData);
23: lblLog.Text = result;
在做這步的時候,需要注意,test.Headers.Add("Content-Type", "application/json") 和test.Headers.Add("ContentLength", byteSend.Length.ToString())需要添加,否則會造成Http 400 返回的錯誤。並且,向服務端傳遞實體的時候,可以通過使用UploadData的方式來進行,如果數據量過大,可以考慮使用異步方式傳送。
接下來的POST和DELETE方法和上面類似,我都貼一下:
POST方法:
1: BookEntity bookEntity = new BookEntity();
2: bookEntity.BookID = Int32.Parse(txtBookID.Text);
3: bookEntity.BookName = txtBookName.Text;
4: bookEntity.BookPrice = decimal.Parse(txtBookPrice.Text);
5: bookEntity.BookPublish = txtBookPublish.Text;
6:
7: DataContractJsonSerializer obj = new DataContractJsonSerializer(typeof(BookEntity));
8: MemoryStream ms = new MemoryStream();
9: obj.WriteObject(ms, bookEntity);
10: byte[] byteSend = ms.ToArray();
11: ms.Close();
12:
13: string serviceURL = string.Format("http://localhost:45345/BookService");
14:
15: WebClient test = new WebClient();
16: test.Headers.Add("Content-Type", "application/json");
17: test.Headers.Add("ContentLength", byteSend.Length.ToString());
18:
19: byte[] responseData = test.UploadData(serviceURL, "POST", byteSend);
20:
21: string result = Encoding.GetEncoding("UTF-8").GetString(responseData);
22: lblLog.Text = result;
DELETE方法:
1: BookEntity bookEntity = new BookEntity();
2: bookEntity.BookID = Int32.Parse(txtBookID.Text);
3:
4: DataContractJsonSerializer obj = new DataContractJsonSerializer(typeof(BookEntity));
5: MemoryStream ms = new MemoryStream();
6: obj.WriteObject(ms, bookEntity);
7: byte[] byteSend = ms.ToArray();
8: ms.Close();
9:
10: string serviceURL = string.Format("http://localhost:45345/BookService");
11:
12: WebClient test = new WebClient();
13: test.Headers.Add("Content-Type", "application/json");
14: test.Headers.Add("ContentLength", byteSend.Length.ToString());
15:
16:
17: byte[] responseData = test.UploadData(serviceURL, "DELETE", byteSend);
18:
19: string result = Encoding.GetEncoding("UTF-8").GetString(responseData);
20: lblLog.Text = result;
最后得到的效果圖如下:
(新增記錄)
(更新記錄)
(刪除記錄)
成文倉促,難免有誤,還請指出,在此謝過。
源代碼下載
Edit:基於本方法構建的Android服務已經在使用中。后續繼續跟進各種使用信息。
在StackOverFlow問答如下:點擊這里查看
看評論中提到了IContract問題,由於這是Restful Service,不是基於RPC模式,所以沒必要使用的。不過用上去也沒錯。