作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 發(fā)布于 2008年12月3日 下午7時(shí)28分
我們已習(xí)慣于在大型中間件平臺(tái)(比如那些實(shí)現(xiàn)CORBA、Web服務(wù)協(xié)議棧和J2EE的平臺(tái))之上構(gòu)建分布式系統(tǒng)了。在這篇文章里,我們將采取另一種做法:我們把支撐Web運(yùn)行的協(xié)議和文檔格式視為一種應(yīng)用平臺(tái),一種可通過輕量級(jí)中間件訪問的平臺(tái)。我們通過一個(gè)簡單的客戶-服務(wù)交互的例子,展示了Web在應(yīng)用集成中的作用。在這篇文章里,我們以Web為主要設(shè)計(jì)理念,提煉并分享了我們下本書《GET /connected - Web-based integration》(暫定名稱)里的一些想法。
相關(guān) 廠商 內(nèi)容
QClub:開源世界里誰需要標(biāo)準(zhǔn)?(12.13 北京)
InfoQ中文站電子雜志《架構(gòu)師》試刊第二期發(fā)布了
SOY Framework:Java富客戶端快速開發(fā)框架
通過DBA管理工具包優(yōu)化數(shù)據(jù)環(huán)境
相關(guān)贊助商
InfoQ中文站SOA社區(qū) ,報(bào)道關(guān)于大中型企業(yè)內(nèi)面向服務(wù)架構(gòu)的一切,目前全球唯一從技術(shù)人的角度持續(xù)關(guān)注SOA的社區(qū)。
引言
我們知道,集成領(lǐng)域是不斷變化的。Web的影響以及敏捷實(shí)踐的潮流正在挑戰(zhàn)我們的關(guān)于“良好的集成由什么構(gòu)成”的觀念。集成(integration)并不是一種夾在系統(tǒng)之間的專業(yè)活動(dòng);與此相反,現(xiàn)在,集成是成功方案里的不可缺少的一部分。
然而,仍有許多人誤解并低估Web在企業(yè)計(jì)算中的作用。即便是那些精通Web的人士,也常常要花費(fèi)很大力氣才能懂得,Web不是關(guān)于支持XML over HTTP的中間件方案,也不是一種簡易的RPC機(jī)制。這是相當(dāng)遺憾的,因?yàn)閃eb不是僅能提供簡單的點(diǎn)對(duì)點(diǎn)連接,它還有更大的用處;它實(shí)際上是一個(gè)健壯的集成平臺(tái)。
在這篇文章里,我們將展示W(wǎng)eb的一些值得關(guān)注的用途,我們將視之為一種可塑的、健壯的平臺(tái),它能夠?qū)ζ髽I(yè)系統(tǒng)做很“酷”的事。另外,工作流是企業(yè)軟件最具代表性的特征。
為什么要工作流?
工作流(workflows)是企業(yè)計(jì)算的主要特征,它們基本上都是用中間件實(shí)現(xiàn)的(至少在計(jì)算方面)。工作流把一項(xiàng)工作(work)劃分為多個(gè)離散的步驟(steps)以及觸發(fā)步驟轉(zhuǎn)移的事件(events)。工作流所實(shí)現(xiàn)的整個(gè)業(yè)務(wù)流程常常跨越若干企業(yè)信息系統(tǒng),這給工作流帶來很多集成問題。
星巴克:統(tǒng)一標(biāo)準(zhǔn)的咖啡需要統(tǒng)一標(biāo)準(zhǔn)的集成
Web若要成為可用于企業(yè)集成的技術(shù),它就必須支持工作流——從而可靠地協(xié)調(diào)不同系統(tǒng)間的交互,以實(shí)現(xiàn)更大的業(yè)務(wù)能力。
要恰如其份地介紹工作流,就免不了講述一大堆跟領(lǐng)域相關(guān)的技術(shù)細(xì)節(jié),而這不是本文的主旨,因此,我們選擇了Gregor Hohpe的星巴克工作流這個(gè)比較好理解的例子來舉例說明基于Web的集成的工作原理。 在這篇受到大家歡迎的博客文章里 ,Gregor講述了星巴克是如何形成一個(gè)解耦合的(decoupled)盈利生產(chǎn)線的:
“跟大部分餐飲企業(yè)一樣,星巴克也主要致力于將訂單處理的吞吐量最大化。顧客訂單越多,收入就越多。為此,他們采取了異步處理的辦法。你在點(diǎn)單時(shí),收銀員取出一只咖啡杯,在上面作上記號(hào)表明你點(diǎn)的是什么,然后把這個(gè)杯子放到隊(duì)列里去。這里的隊(duì)列指的是在咖啡機(jī)前排成一列的咖啡杯。正是這個(gè)隊(duì)列將收銀員與咖啡師解耦開,從而,即便在咖啡師一時(shí)忙不過來的時(shí)候,收銀員仍然可以為顧客點(diǎn)單。他們可以在繁忙時(shí)段安排多個(gè)咖啡師,就像競爭消費(fèi)者模式(Competing Consumer)里那樣。”
Gregor是采用EAI技術(shù)(如面向消息的中間件)來講解星巴克案例的,而我們將采用Web資源(支持統(tǒng)一接口的可尋址實(shí)體)來講解同一案例。實(shí)際上,我們將展示W(wǎng)eb技術(shù)何以能夠具有跟傳統(tǒng)EAI工具一樣的可靠性,以及何以不僅僅是請(qǐng)求/響應(yīng)協(xié)議之上的XML消息傳遞!
首先,我們很抱歉擅自設(shè)想了星巴克的工作流程,因?yàn)槲覀兊哪康牟⒉皇蔷_無誤地描述星巴克,而是用基于Web的服務(wù)來講解工作流。好的,既然講清楚了這一點(diǎn),那么我們現(xiàn)在開始吧。
簡明陳述
因?yàn)槲覀冊谥v工作流,所以我們有必要理解構(gòu)成工作流的狀態(tài)(states)以及將工作流從一個(gè)狀態(tài)轉(zhuǎn)移到另一個(gè)狀態(tài)的事件(events)。我們的例子里有兩個(gè)工作流,我們把它們用狀態(tài)機(jī)(state machines)表達(dá)出來了。這兩個(gè)工作流是并行執(zhí)行的。一個(gè)反映了顧客與星巴克服務(wù)之間的交互(如圖1),另一個(gè)刻畫了由咖啡師執(zhí)行的一系列動(dòng)作(如圖2)。
在顧客工作流里,顧客為了得到某種口味的咖啡而與星巴克服務(wù)進(jìn)行交互。我們假定該工作流里包含以下動(dòng)作:顧客點(diǎn)單,付款,然后等待飲品。在點(diǎn)單與付款之間,顧客通常可以修改菜單,比方說請(qǐng)求改用半脫脂牛奶。 /p>
圖1 顧客狀態(tài)機(jī)
盡管顧客看不見咖啡師,但咖啡師也有自己的狀態(tài)機(jī);這個(gè)狀態(tài)機(jī)是服務(wù)實(shí)現(xiàn)私有的。如圖2所示,咖啡師在周而復(fù)始地等待下一個(gè)訂單,制作飲品,然后收取費(fèi)用。當(dāng)一個(gè)訂單被加入到咖啡師的隊(duì)列中時(shí),一次循環(huán)實(shí)例就開始了。當(dāng)咖啡師完成訂單并把飲品交付給顧客時(shí),工作流就結(jié)束了。
圖2 咖啡師的狀態(tài)機(jī)
盡管這些看似跟基于Web的集成毫不相干,但這兩個(gè)狀態(tài)機(jī)里的每一個(gè)狀態(tài)遷移,都代表著與Web資源的一次交互。每一次遷移,就是通過URI對(duì)資源實(shí)施HTTP操作,從而導(dǎo)致狀態(tài)的改變。
GET和HEAD屬于特例,因?yàn)樗鼈儾灰馉顟B(tài)遷移。它們的作用是用于查看資源的當(dāng)前狀態(tài)。
我們節(jié)奏稍快了點(diǎn)。理解狀態(tài)機(jī)和Web,不是那么容易一口吃個(gè)胖子的。所以,讓我們在Web的背景下,來從頭回顧一下整個(gè)場景,逐步慢慢深入。
顧客視角
我們將從一張簡單的故事卡片開始,它啟動(dòng)整個(gè)流程:
這個(gè)故事里涉及一些有用的角色與實(shí)體。首先,里面有“顧客(Customer)”角色。顯然,它是(隱含的)星巴克服務(wù)(Starbucks Service)的消費(fèi)者。其次,里面有兩個(gè)重要的實(shí)體(“咖啡”和“訂單”),以及一個(gè)重要的交互(“點(diǎn)單”)——我們的工作流正是由它啟動(dòng)的。
要把訂單提交給星巴克,我們只要把訂單的表示(representation)POST給下面這個(gè)眾所周知的星巴克點(diǎn)單URI即可:
http://starbucks.example.org/order
。
圖3 點(diǎn)一杯咖啡
圖3顯示了向星巴克點(diǎn)單的交互過程。星巴克采用自己的XML格式來表達(dá)有關(guān)實(shí)體;需要關(guān)注的是,這個(gè)格式允許客戶往里嵌入信息,以便進(jìn)行點(diǎn)單——稍后我們會(huì)看到。實(shí)際提交的數(shù)據(jù)如圖4所示。
在面向人類的Web(human Web)上,消費(fèi)者和服務(wù)使用HTML作為表示格式(representation format)。HTML有自己特定的語義,所有瀏覽器都理解并接受這些語義,比如: 代表 “一個(gè)鏈接到其他文檔或本文檔內(nèi)部某個(gè)書簽的錨(anchor)”。消費(fèi)者應(yīng)用——瀏覽器——只是呈現(xiàn)HTML,狀態(tài)機(jī)(也就是你!)用GET
和POST
跟隨鏈接。對(duì)于基于Web的集成也一樣,只不過服務(wù)和消費(fèi)者不僅要就交互協(xié)議達(dá)成一致,還要就表示的格式與語義統(tǒng)一意見。
圖4 POST飲品訂單
星巴克服務(wù)創(chuàng)建一個(gè)訂單資源,然后把這個(gè)新資源的位置放在HTTP報(bào)頭
Location
里返回給消費(fèi)者。為方便起見,服務(wù)還要把這個(gè)新創(chuàng)建的訂單資源的表示(representation)也放在響應(yīng)里。發(fā)給消費(fèi)者的響應(yīng)如下所示。
圖5 創(chuàng)建好了訂單,等待付款
201 Created
狀態(tài)表明星巴克已經(jīng)成功接受了訂單。
Location
報(bào)頭給出了新創(chuàng)建訂單的URI。響應(yīng)主體里的表示(representation)包含了所點(diǎn)飲品及其價(jià)格。另外,這個(gè)表示里還包含另一個(gè)資源的URI——星巴克希望我們與這個(gè)URI交互,以完成顧客工作流;我們稍后將用到它。
注意,該URI是放在
<next></next>
標(biāo)簽、而不是標(biāo)簽里的。這里的
<next></next>
在顧客工作流里是具有特定含義的,其語義是事先定義好的。
我們已經(jīng)知道201 Created
狀態(tài)代碼表示“成功創(chuàng)建資源”的意思。對(duì)于這個(gè)例子以及一般的基于Web的集成,我們還需要其他一些有用的代碼:
200 OK
——它的意思是:一切正常;繼續(xù)執(zhí)行。
201 Created
——我們剛剛創(chuàng)建了一個(gè)資源,一切正常。
202 Accepted
——服務(wù)已經(jīng)接受了我們的請(qǐng)求,并請(qǐng)我們對(duì)Location響應(yīng)報(bào)頭里的URI進(jìn)行輪詢(poll)。這在異步處理中相當(dāng)有用。
303 See Other
——我們需要跟另一個(gè)資源交互,應(yīng)該不會(huì)出錯(cuò)。
400 Bad Request
——我們的請(qǐng)求格式有問題,應(yīng)重新格式化后再提交。
404 Not Found
——服務(wù)因?yàn)橥祽校ɑ蛘弑C埽]有告知請(qǐng)求失敗的真實(shí)原因,但不管什么原因,我們都得應(yīng)付它。
409 Conflict
——服務(wù)器拒絕了我們更新資源狀態(tài)的請(qǐng)求。我們需要獲取資源的當(dāng)前狀態(tài)(要么檢查響應(yīng)實(shí)體主體,要么做一次GET操作),然后再作打算。
412 Precondition Failed
——請(qǐng)求未被處理,因?yàn)镋tag、If-Match或類似的“哨兵(guard)”報(bào)頭的值不滿足條件。我們需要考慮下一步怎么走。
417 Expectation Failed
——幸虧核查一下,服務(wù)器不將接受你的請(qǐng)求,所以別真正發(fā)送那個(gè)請(qǐng)求。
500 Internal Server Error
——最偷懶的響應(yīng)。服務(wù)器出錯(cuò)了,而且什么原因都沒說。祝你不要碰見它。
更新訂單
星巴克很不錯(cuò)的一點(diǎn)就是,你可以按無數(shù)種不同的方式來定制自己的飲品。其實(shí),考慮到某些高端客戶極高的要求,也許讓他們按化學(xué)公式來點(diǎn)單更好。但我們別那么貪心——至少開始的時(shí)候。我們來看另一張故事卡片:
回顧圖4,顯然我們在那里犯了一個(gè)錯(cuò)誤:真正愛喝咖啡的人是不喜歡往濃咖啡里放太多熱牛奶的。我們要改正那個(gè)問題。幸運(yùn)地是,Web(或更確切地說,HTTP)以及我們的服務(wù)均為這樣的改變提供了支持。
首先,我們要確認(rèn)我們?nèi)匀豢梢孕薷挠唵巍S袝r(shí)咖啡師動(dòng)作很快,在我們想修改訂單之前,他們就已經(jīng)把咖啡做好了——于是,我們只有慢慢享用這杯熱咖啡風(fēng)味的牛奶了。不過,有時(shí)咖啡師會(huì)比較慢,這樣我們就可以在訂單得到咖啡師處理之前修改它了。為了知道我們是否還能修改訂單,我們通過HTTP動(dòng)詞OPTIONS來向訂單資源查詢它接受哪些操作(如圖6)。
請(qǐng)求 | 響應(yīng) |
OPTIONS /order/1234 HTTP 1.1 Host: starbucks.example.org
|
200 OK Allow: GET, PUT
|
圖6 看看有哪些選擇(OPTIONS)
從圖6我們可以知道,訂單資源既是可讀的(支持GET)、也是可更新的(支持PUT)。作為好網(wǎng)民,我們可以拿我們的新表示來做一次試驗(yàn)性的PUT操作,在真正PUT之前先用
Expect
報(bào)頭來試一試(如圖7)。
請(qǐng)求 | 響應(yīng) |
PUT /order/1234 HTTP 1.1 Host: starbucks.example.com Expect: 100-Continue
|
100 Continue
|
圖7 看好再做(Look before you leap)
若我們不能修改訂單了,那么對(duì)圖7所示請(qǐng)求的響應(yīng)將是
417 Expectation Failed
。不過,假定我們現(xiàn)在得到的響應(yīng)是
100 Continue
,也就是說,我們可以用
PUT
來更新訂單資源(如圖8)。用
PUT
方法來提交更新后的資源表示(representation),實(shí)際上就相當(dāng)于修改現(xiàn)有資源。在這個(gè)例子中,PUT請(qǐng)求里的新描述包含一個(gè)
<additions></additions>
元素,其中包含我們的更新,即外加一杯濃咖啡。
盡管部分更新(partial updates)屬于REST社區(qū)里比較難懂的理念爭論之一,但這里我們采取一種實(shí)用的做法,我們假定:增加一杯濃咖啡的請(qǐng)求,是在現(xiàn)有資源狀態(tài)的上下文中被處理的。因此,我們沒必要在網(wǎng)絡(luò)上傳送整個(gè)資源表示,我們只要傳送變化的部分即可。
圖8 更新資源狀態(tài)
如果我們能夠成功提交(
PUT
)更新,那么我們會(huì)從服務(wù)器得到響應(yīng)代碼
200
,如圖9所示。
圖9 成功更新資源狀態(tài)
檢查
OPTIONS
和采用
Expect
報(bào)頭并不能令我們避免碰到“后續(xù)的修改請(qǐng)求失敗”的情況。因此,我們并不強(qiáng)制使用它。作為好網(wǎng)民,我們會(huì)以某種方式來應(yīng)付
405
和
409
響應(yīng)。
OPTIONS和
Expect
報(bào)頭的使用應(yīng)當(dāng)被視為可選步驟。
盡管我們明智地使用
Expect
和
OPTIONS
,但有時(shí)
PUT
仍將失敗;畢竟咖啡師也在一刻不停地工作——有時(shí)他們動(dòng)作很敏捷!
若我們落后于咖啡師,我們在試圖用
PUT
操作把更新提交給資源時(shí)會(huì)被告知。圖10顯示的就是一個(gè)常見的更新失敗的響應(yīng)。
409 Conflict
狀態(tài)代碼表明,若接受更新,將導(dǎo)致資源處于不一致的狀態(tài),所以沒有進(jìn)行更新。響應(yīng)主體里顯示出了我們試圖
PUT
的表示(representation)與服務(wù)端資源狀態(tài)之間的差異。按咖啡制作的話說,加得太晚了——咖啡師已經(jīng)把熱牛奶倒進(jìn)去了。
圖10 慢了一步
我們已經(jīng)講述了使用
Expect
和
OPTIONS
來盡量防止競爭條件。除此以外,我們還可以給我們的
PUT
請(qǐng)求加上
If-Unmodified-Since
或
If-Match
報(bào)頭,以表達(dá)我們對(duì)服務(wù)的期望條件。
If-Unmodified-Since
采用時(shí)間戳,而
If-Match
采用原始訂單的ETag
1
。若訂單狀態(tài)自從被我們創(chuàng)建以來還沒有改變過——也就是說,咖啡師還沒有開始制作我們的咖啡——那么更新可以處理。若訂單狀態(tài)已經(jīng)發(fā)生改變,那么我們會(huì)得到
412 Precondition Failed
響應(yīng)。雖然我們因?yàn)槁丝Х葞熞徊蕉荒芟碛门D炭Х龋辽傥覀儧]有把資源轉(zhuǎn)移到不一致的狀態(tài)。
用Web進(jìn)行一致的狀態(tài)更新可以采取很多種模式。HTTP PUT是冪等的(idempotent),這樣我們在進(jìn)行狀態(tài)更新時(shí)就用不著處理一些復(fù)雜事務(wù)了,不過仍有一些選擇需要我們決定。下面是正確進(jìn)行狀態(tài)更新的一些方法:
1. 通過發(fā)送
OPTIONS
請(qǐng)求,查詢服務(wù)是否接受PUT
操作。這一步是可選的。它可以告知客戶端,此刻服務(wù)器允許對(duì)該資源做哪些操作,不過這無法保證服務(wù)器將永遠(yuǎn)支持那些操作。2. 使用
If-Unmodified-Since
或If-Match
報(bào)頭,以避免服務(wù)器執(zhí)行不必要的PUT
操作。假如PUT
后來失敗了,那么你會(huì)得到412 Precondition Failed
。此方法要求:要么資源是緩慢更新的,要么支持ETag;對(duì)于前者就用If-Unmodified-Since
,對(duì)于后者就用If-Match
。3. 立即用
PUT
操作提交更新,并應(yīng)付可能出現(xiàn)的409 Conflict
響應(yīng)。就算我們使用了(1)和(2),我們可能仍得應(yīng)付這些響應(yīng),因?yàn)槲覀兊摹吧诒焙蜋z查本質(zhì)上都是樂觀的。關(guān)于檢測和處理不一致的更新,W3C有 一個(gè)非規(guī)范性文檔 ,該文檔推薦采用ETag。ETags也是我們推薦采用的方法。
在完成那些更新咖啡訂單的艱苦工作之后,按理說我們應(yīng)當(dāng)?shù)玫筋~外那杯濃咖啡了。所以我們現(xiàn)在假定已設(shè)法得到了額外那杯濃咖啡。當(dāng)然,我們要付過款后星巴克才會(huì)把咖啡遞給我們(其實(shí)他們也已經(jīng)暗示過了!),所以我們還需要一張故事卡片:
還記得最初那個(gè)針對(duì)原始訂單的響應(yīng)嗎?其中有個(gè)
<next></next>
元素。星巴克在訂單資源的表示里面嵌入了有關(guān)另一個(gè)資源的信息。我們前面看過那個(gè)標(biāo)簽,但當(dāng)時(shí)因?yàn)轭櫽谛薷挠唵尉蜎]有具體講。現(xiàn)在我們應(yīng)該進(jìn)一步探討它了:
關(guān)于
next
元素,有幾點(diǎn)是值得指出的。首先,它處于一個(gè)不同的名稱空間之下,因?yàn)闋顟B(tài)遷移并不是只有星巴克需要。在這里,我們決定把這種用于狀態(tài)遷移的URI放在一個(gè)公共的名稱空間里,以便于重用(或甚至最終的標(biāo)準(zhǔn)化)。
其次,
rel
屬性里嵌入了一則語義信息(你樂意的話,也可以稱之為一種私有的微格式)。能夠理解
http://starbucks.example.org/payment
這串文字的消費(fèi)者,可以使用由
uri
屬性標(biāo)識(shí)的資源轉(zhuǎn)移到工作流里的下一狀態(tài)(付款)。
<next></next>
元素里的
uri
指向的是一個(gè)付款資源。根據(jù)
type
屬性,我們已經(jīng)知道預(yù)期的資源表示(representation)是XML格式的。我們可以向這個(gè)付款資源發(fā)送
OPTIONS
請(qǐng)求,看看它支持哪些HTTP操作。
微格式(microformat)是一種在現(xiàn)有文檔里嵌入結(jié)構(gòu)化、語義豐富的數(shù)據(jù)的方式。微格式在人類可讀的Web上相當(dāng)常見,它們用于往網(wǎng)頁里增加結(jié)構(gòu)化信息(如日程表)的 表示(representations)。不過,它們同樣也可以方便地被用于集成。微格式術(shù)語是在微格式社區(qū)里達(dá)成一致的,不過我們也可以自由創(chuàng)建自己的 私有微格式,用于特定領(lǐng)域的語義標(biāo)記。
盡管它們看上去沒多大用,但如圖10里那樣的簡單鏈接正是REST社區(qū)所呼吁的“將超媒體作為應(yīng)用狀態(tài)的引擎(hypermedia as the engine of application state)”的關(guān)鍵。更簡單地說,URI代表了狀態(tài)機(jī)里的狀態(tài)遷移。正如我們在文章開始時(shí)所看到的,客戶端是通過跟隨鏈接的方式來操作應(yīng)用程序的狀態(tài) 機(jī)的。
如果你一時(shí)不能理解,不要感到奇怪。這一模型的最不可思議之處在于:狀態(tài)機(jī)和工作流不是像WS-BPEL或WS-CDL那樣事先描述好的,而是在你 經(jīng)歷各個(gè)狀態(tài)的過程中逐步得到描述的。不過,一旦你的想明白了,你就會(huì)發(fā)現(xiàn),跟隨鏈接(following links)這種方式使得我們可以在應(yīng)用的各種狀態(tài)下向前推進(jìn)。每次狀態(tài)遷移時(shí),當(dāng)前資源的表示里都包含了指向可能的下一狀態(tài)的鏈接以及它們所代表的狀 態(tài)。另外,由于這些代表下一狀態(tài)的資源是Web資源,所以我們知道如何使用它們。
在顧客工作流里,我們下一步要做的是為咖啡付款。我們可以由訂單里的
<cost></cost>
元素得知總金額,但在我們向星巴克付款之前,我們想向付款資源查詢一下我們應(yīng)當(dāng)如何與之交互(如圖11)。
消費(fèi)者需要事先掌握多少關(guān)于一個(gè)服務(wù)的知識(shí)呢?我們已經(jīng)說過了,服務(wù)和消費(fèi)者在交互之前需要就它們將會(huì)交換的表示(representations)的語 義達(dá)成一致。可以將這些表示格式(representation formats)看成一組可能的狀態(tài)和遷移。在消費(fèi)者與服務(wù)交互時(shí),服務(wù)選擇可用的狀態(tài)和遷移,并構(gòu)造下一個(gè)表示。步向目標(biāo)的過程是 動(dòng)態(tài)發(fā)現(xiàn)的,而把這一過程中的各個(gè)部分串起來的方式是事先達(dá)成一致的。在設(shè)計(jì)與開發(fā)過程中,消費(fèi)者會(huì)就表示和遷移的語義與服務(wù)器達(dá)成一致。但誰也不能保證服務(wù)在其演化過程中會(huì)不會(huì)采用一種客戶端預(yù)期之外的表示和遷移 (不過客戶端還是知道如何處理它的)——那是Web松耦合的本質(zhì)特性。盡管如此,在這些情況下就資源格式和表示達(dá)成一致超出了本文的范圍。
我們下一步要做的是為咖啡付款。我們可以由訂單表示的
<cost></cost>
元素得知總金額,所以我們要做的就是付款給星巴克,然后咖啡師把飲品交給我們。首先,我們向付款資源查詢我們應(yīng)當(dāng)如何與之交互(如圖11)。
請(qǐng)求 | 響應(yīng) |
OPTIONS/payment/order/1234 HTTP 1.1 Host: starbucks.example.com
|
Allow: GET, PUT
|
圖11 獲知如何付款
服務(wù)器返回的響應(yīng)告訴我們,我們既可以讀取付款(通過
GET
)、也可以更新它(通過
PUT
)。既然知道了金額,那么接下來,我們就把款項(xiàng)
PUT
給那個(gè)由付款鏈接標(biāo)識(shí)的資源。當(dāng)然,付款金額屬于秘密信息,所以我們將通過認(rèn)證
2
來保護(hù)該資源。
請(qǐng)求 |
PUT /payment/order/1234 HTTP 1.1
|
響應(yīng) |
201 Created
|
圖12 付款
為成功完成付款,我們只需按圖12進(jìn)行交互即可。一旦經(jīng)認(rèn)證的
PUT
返回一個(gè)
201 Created
響應(yīng),我們就可以慶祝付款成功、并拿到我們的飲品了。
不過事情也有出錯(cuò)的時(shí)候。當(dāng)資金處于危險(xiǎn)狀態(tài)時(shí),我們希望要么沒出錯(cuò)、要么可以挽救錯(cuò)誤 3 。付款時(shí)可能出現(xiàn)很多種容易想象的出錯(cuò)情況:
- 由于服務(wù)器宕機(jī)或其他原因,我們無法連接上服務(wù)器了;
- 在交互過程中,與服務(wù)器的連接被切斷了;
-
服務(wù)器返回一個(gè)
4xx
或5xx
范圍的錯(cuò)誤狀態(tài)。
幸運(yùn)地是,Web可以幫助我們應(yīng)付以上這些情況。對(duì)前兩種情況(假定連接問題是瞬間的),我們可以反復(fù)做
PUT
請(qǐng)求,直至我們收到成功響應(yīng)為止。如果前次
PUT
操作已經(jīng)得到了成功處理,那么我們將收到一個(gè)
200
響應(yīng)(本質(zhì)上是一個(gè)來自服務(wù)器的空操作確認(rèn));如果本次
PUT
操作成功完成了付款,那么我們將收到一個(gè)
201
響應(yīng)。在第三種情況中,如果服務(wù)器返回的響應(yīng)代碼是
500
、
503
或
504
,那么也可以做同樣處理。
4xx
范圍的狀態(tài)代碼比較難處理,不過它們?nèi)匀恢赋隽讼乱徊皆趺崔k。例如,
400
響應(yīng)表明我們通過
PUT
請(qǐng)求提交的內(nèi)容無法被服務(wù)器所理解,我們需要糾正后重新發(fā)送
PUT
請(qǐng)求。
403
響應(yīng)則相反,它表明服務(wù)器能夠理解我們的請(qǐng)求,但不知道如何履行(fulfil)它,而且服務(wù)器希望我們不要重試。對(duì)于這些情況,我們得在響應(yīng)的有效負(fù)載(payload)里尋找其他的狀態(tài)遷移(鏈接),換其他推進(jìn)狀態(tài)的路線。
在這個(gè)例子中,我們已經(jīng)多次使用狀態(tài)代碼來指引客戶端步向下一個(gè)交互了。狀態(tài)代碼是具有豐富語義的確認(rèn)信息。讓服務(wù)返回有意義狀態(tài)代碼,并且令客戶 端懂得如何處理狀態(tài)代碼,這樣一來,我們便給HTTP簡單的請(qǐng)求響應(yīng)機(jī)制增加了一層協(xié)調(diào)協(xié)議,從而提高了分布式系統(tǒng)的健壯性和可靠性。
一旦我們?yōu)樽约旱娘嬈焚I了單,我們這個(gè)工作流就算完成了,有關(guān)顧客的故事也就到此結(jié)束了。不過整個(gè)故事還沒有完。現(xiàn)在我們進(jìn)入到服務(wù)里面,看看星巴克的內(nèi)部實(shí)現(xiàn)。
咖啡師視角
作為顧客,我們樂于把自己放在咖啡世界的中央,不過我們并不是咖啡服務(wù)的唯一消費(fèi)者。從與咖啡師的“時(shí)間競賽”中我們已經(jīng)得知,咖啡服務(wù)還為包括咖啡師在內(nèi)的其他一些相關(guān)方面提供服務(wù)。按照我們循序漸進(jìn)的介紹方式,現(xiàn)在該推出另一張故事卡片了。
用Web的格式與協(xié)議來描述飲品列表是件很容易的事。用Atom提要(feeds)來表達(dá)列表之類的東西是相當(dāng)不錯(cuò)的選擇,它幾乎可描述任何列表(比如未完成的咖啡訂單),所以這里我們可以也采用它。咖啡師可以通過向該Atom提要的URI發(fā)送
GET
請(qǐng)求來訪問它,對(duì)于未完成的訂單,URI是
http://starbucks.example.org/orders
(如圖13)。
圖13 待制作飲品的Atom提要
星巴克是家相當(dāng)繁忙的店,位于
/orders
的Atom提要更新相當(dāng)頻繁,所以咖啡師要不斷輪詢這個(gè)提要才能保證掌握最新信息。輪詢通常被認(rèn)為可伸縮性很差;但是,Web支持可伸縮性極強(qiáng)的輪詢機(jī)制——我們稍后會(huì)看到。另外,由于星巴克每分鐘要制作很多咖啡,所以承受住負(fù)荷是個(gè)重要問題。
這里我們有兩個(gè)相抵觸的需求。一方面,我們希望咖啡師通過經(jīng)常輪詢訂單提要,以不斷掌握最新信息;另一方面,我們又不希望給服務(wù)增添負(fù)擔(dān)、或者徒然增加網(wǎng) 絡(luò)流量。為防止我們的服務(wù)因過載而崩潰,我們將在我們服務(wù)之外,用一個(gè)逆向代理(reverse proxy)來緩存并提供被頻繁訪問的資源表示(如圖14所示)。
圖14 通過緩存提升可伸縮性
對(duì)于大多數(shù)資源(尤其是那些會(huì)被很多人訪問的資源,如返回飲品列表的Atom提要),在宿主服務(wù)之外緩存它們是合理的。這樣可以降低服務(wù)器負(fù) 載,提升可伸縮性。我們在架構(gòu)里增設(shè)了Web緩存(逆向代理),再加上有緩存元數(shù)據(jù),這樣客戶端獲取資源時(shí)就不會(huì)給原服務(wù)器增添很大負(fù)擔(dān)了。
緩存的有利一面是,它屏蔽掉了服務(wù)器的間隙性故障,并通過提高資源可用率來幫助災(zāi)難恢復(fù)。也就是說,即便星巴克服務(wù)出現(xiàn)了故障,咖啡師仍然可以繼續(xù)工 作,因?yàn)橛唵涡畔⑹潜淮砭彺嫫饋淼摹6遥偃缈Х葞熉┝四硞€(gè)訂單的話(錯(cuò)誤),恢復(fù)也很容易進(jìn)行,因?yàn)橛唵尉哂泻芨叩目捎寐省?
是的,緩存可以把舊訂單多保留一段時(shí)間,但對(duì)于像星巴克這樣吞吐量很高的商戶而言,這是不太理想的。為了把太舊的訂單從緩存中清除,星巴克服務(wù)用
Expires
報(bào)頭來聲明一個(gè)響應(yīng)可以被緩存多久。任何介于消費(fèi)者與服務(wù)之間的緩存都應(yīng)當(dāng)服從這一指示,拒絕提供過期訂單 4 ,而是把請(qǐng)求轉(zhuǎn)發(fā)到星巴克服務(wù)上,以獲取最新的訂單信息。
圖13所示的響應(yīng)對(duì)Atom提要的
Expires
報(bào)頭進(jìn)行了相應(yīng)的設(shè)置,令飲品列表在10秒鐘后過期。由于這 種緩存行為,服務(wù)器每分鐘最多只要響應(yīng)6次請(qǐng)求,其余請(qǐng)求將由緩存機(jī)制代勞。即便對(duì)于性能比較糟糕的服務(wù),每分鐘6個(gè)請(qǐng)求也屬于容易處理的工作量了。在最 愉快的情況下(對(duì)星巴克服務(wù)來說),咖啡師的輪詢請(qǐng)求是由本地緩存響應(yīng)的,這樣就不會(huì)給增加網(wǎng)絡(luò)活動(dòng)或服務(wù)器負(fù)荷了。
在我們的例子中,我們只設(shè)置了一個(gè)緩存來幫助提升主咖啡列表的可伸縮性。然而,在真實(shí)的基于Web的場景中,我們可以從多層緩存中受益。要在大規(guī)模環(huán)境中提升可伸縮性,利用現(xiàn)有Web緩存的優(yōu)點(diǎn)是至關(guān)重要的。
Web以延遲換取了高度的可伸縮性。假如你的問題對(duì)延遲很敏感的話(比如外匯交易),那么就不太適合采用基于Web的方案了。但是,假如你可以接受“秒”數(shù)量級(jí)上的延遲,那么Web也許是個(gè)不錯(cuò)的平臺(tái)。
既然我們已經(jīng)成功解決了可伸縮性問題,那么我們繼續(xù)來實(shí)現(xiàn)更多的功能。當(dāng)咖啡師開始為你制作咖啡時(shí),應(yīng)當(dāng)修改訂單狀態(tài),以達(dá)到禁止更新的目的。從顧客的角度來看,這相當(dāng)于我們無法再對(duì)我們的訂單執(zhí)行
PUT
操作了(如圖6、7、8、9、10所示)。
幸運(yùn)地是,我們可以利用一個(gè)已經(jīng)定義好的協(xié)議——Atom發(fā)布協(xié)議(Atom Publishing Protocol,簡稱APP或AtomPub)——來實(shí)現(xiàn)這一目標(biāo)。AtomPub是一個(gè)以Web中心(基于URI)的協(xié)議,用于管理Atom提要里的 條目(entries)。我們來仔細(xì)看看Atom提要(
/orders
)里代表咖啡的條目。
圖15 咖啡訂單對(duì)應(yīng)的Atom條目
在圖15所示的XML里,有幾點(diǎn)值得注意。首先,它將我們的訂單與Atom提要里的其他訂單區(qū)分開了。其次,其中包含訂單本身,即咖啡師制作咖啡所需的全部信息——包括我們要求增加一杯濃咖啡的重要信息!該訂單對(duì)應(yīng)的
entry
元素里有個(gè)
link
元素,它聲明了本條目(
entry
)的編輯URI(
edit
)。這個(gè)編輯URI指向的是一個(gè)可以通過HTTP編輯的訂單資源。(這里,可編輯資源的地址剛好跟訂單資源本身的地址一樣,不過這不是必須的。)
如果咖啡師要鎖定訂單資源、禁止它被修改,就可以通過該編輯URI來改變訂單資源的狀態(tài)。具體地講,咖啡師可以用
PUT
請(qǐng)求把經(jīng)修改的資源狀態(tài)提交給這個(gè)編輯URI(如圖16所示)。
圖16 通過AtomPub設(shè)置訂單狀態(tài)
服務(wù)器一旦處理了如圖16所示的
PUT
請(qǐng)求,它就會(huì)拒絕對(duì)位于
/orders/1234
的訂單資源做除
GET
以外的操作。
現(xiàn)在訂單處于穩(wěn)定狀態(tài)了,咖啡師可以毫無顧慮地繼續(xù)制作咖啡了。當(dāng)然,咖啡師只有知道我們已經(jīng)付過款才會(huì)把咖啡給我們,所以咖啡師還要查詢我們是否已經(jīng)完 成付款。在真實(shí)的星巴克里,情況會(huì)略有不同:一般來說,我們是點(diǎn)單后立即付款的;然后,其他顧客站在周圍,以免你拿走別人點(diǎn)的飲品。但在我們計(jì)算機(jī)化的版 本里,增加這一檢查并不麻煩,所以我們來看倒數(shù)第二張故事卡片:
咖啡師只要向付款資源(該資源的URI在訂單表示里給出了)發(fā)送
GET
請(qǐng)求,即可查詢付款狀態(tài)。
這里,顧客和咖啡師是通過訂單表示里給出的鏈接得知付款資源的URI的。但有時(shí),通過URI模版來訪問資源也很方便。
URI模版(URI template)是一種描述知名URI的格式。它允許消費(fèi)者通過修改URI里的部分字符來訪問不同的資源。
Amazon的S3存儲(chǔ)服務(wù)就是基于URI模版的。用戶可以對(duì)由以下模版生成的URIs進(jìn)行HTTP操作,從而對(duì)已保存的制品進(jìn)行操作:
http://s3.amazonaws.com/{bucket_name}/{key_name}
。為方便咖啡師(或其他經(jīng)授權(quán)的星巴克系統(tǒng))不用遍歷所有訂單即可訪問各個(gè)付款資源,我們可以在我們的模型里設(shè)計(jì)一個(gè)類似的URL模版方案:
http://starbucks.example.org/payment/order/{order_id}
。URI模版就像與消費(fèi)者訂立的契約,服務(wù)提供者須在服務(wù)演化過程中注意維持它們的穩(wěn)定。由于這一潛在的耦合,有些Web集成工作者會(huì)有意避免采用URI模版。我們的建議是,僅當(dāng)可推斷的URIs(inferable URIs)很有幫助而且不會(huì)改變時(shí)才使用。
對(duì)于我們的例子,另一種辦法是在
/payments
處暴露一個(gè)提要,用它提供包含指向各個(gè)付款資源的(不可推斷的)鏈接。該提要只有經(jīng)授權(quán)的系統(tǒng)才能讀取。最終,URI模版是不是一個(gè)相對(duì)超媒體來說安全而有效的捷徑,要由服務(wù)設(shè)計(jì)者來決定。我們的建議是:要保守地使用URI模版!
當(dāng)然,不是人人都可以查看付款信息的。我們不想讓咖啡社區(qū)里會(huì)動(dòng)歪腦筋的人查看他人的信用卡詳細(xì)信息,因此,跟其他敏感的Web系統(tǒng)一樣,我們利用請(qǐng)求認(rèn)證來保護(hù)敏感資源。
如有未認(rèn)證的用戶或系統(tǒng)試圖獲取一個(gè)具體的付款信息,那么服務(wù)器會(huì)質(zhì)詢(challenge)它、要求它提供證書。(如圖17)
請(qǐng)求 | 響應(yīng) |
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org
|
401 Unauthorized WWW-Authenticate: Digest realm="starbucks.example.org", qop="auth", nonce="ab656...", opaque="b6a9..."
|
圖17 對(duì)付款資源的非授權(quán)訪問受到質(zhì)詢
401
狀態(tài)(及其認(rèn)證元數(shù)據(jù))告訴我們,我們應(yīng)當(dāng)在請(qǐng)求里附上正確的證書、然后重新發(fā)送請(qǐng)求。重新用正確的證書發(fā)送請(qǐng)求(圖18)后,我們得到了付款信息,并將之與代表訂單總金額的資源
http://starbucks.example.org/total/order/1234
進(jìn)行比較。
請(qǐng)求 | 響應(yīng) |
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org Authorization: Digest username="barista joe" realm="starbucks.example.org“ nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..."
|
200 OK
|
圖18 授權(quán)訪問付款資源
一旦咖啡師制作好、交出咖啡并完成收款,他們就要在待處理飲品列表中刪除相應(yīng)的訂單。如同前面一樣,我們采用一個(gè)故事來講解這個(gè)回合:
因?yàn)橛唵翁嵋锏母鱾€(gè)條目(
entry
)都標(biāo)識(shí)著一個(gè)可編輯資源,而且有自己的URI,所以我們可以對(duì)各個(gè)訂單資源做HTTP操作。如圖19所示,咖啡師只要對(duì)相關(guān)條目(
entry
)所引用的資源做
DELETE
操作即可將它從列表中刪除。
請(qǐng)求 | 響應(yīng) |
DELETE /order/1234 HTTP 1.1 Host: starbucks.example.org
|
200 OK
|
圖19 刪除已完成的訂單
在條目被刪除(
DELETE
)之后,再對(duì)訂單提要做
GET
操作的話,返回的表示里將不再包含已刪除(
DELETE
)的資源。假定我們的緩存工作正常、且我們已經(jīng)設(shè)置了合理的緩存過期元數(shù)據(jù)的話,那么當(dāng)你試圖獲取(
GET
)那個(gè)訂單條目時(shí)將直接得到
404 Not Found
響應(yīng)。
也許你已經(jīng)注意到了,Atom發(fā)布協(xié)議可以滿足我們對(duì)星巴克這個(gè)問題的大部分需求。如果我們想直接把位于
/orders
的Atom提要暴露給顧客的話,顧客就可以用Atom發(fā)布協(xié)議來向該提要發(fā)布飲品訂單、甚至修改訂單了。
演化:Web上的現(xiàn)實(shí)情況
因?yàn)槲覀兊目Х鹊晔腔谧悦枋龅臓顟B(tài)機(jī)(state machines)構(gòu)建起來的,所以我們可以方便地根據(jù)業(yè)務(wù)需要改造我們的工作流。例如,星巴克也許會(huì)提供一種免費(fèi)的網(wǎng)上促銷活動(dòng):
- 7月——我們的星巴克店開業(yè),并提供標(biāo)準(zhǔn)的工作流以及我們前面提到的狀態(tài)遷移和表示(representation)。消費(fèi)者知道用這些格式與表示跟我們的服務(wù)進(jìn)行交互。
-
8月——星巴克新推出了一種免費(fèi)網(wǎng)上促銷的表示(representation)。我們的咖啡工作流將進(jìn)行更新,以包含指向該網(wǎng)上促銷資源的鏈接。由于URI的特性,鏈接可以是指向第三方的——這跟指向星巴克內(nèi)部的資源一樣簡單。
因?yàn)楸硎纠锶匀话瓉淼倪w移點(diǎn),所以現(xiàn)有消費(fèi)者仍然可以實(shí)現(xiàn)它們的目標(biāo),只不過它們可能無法享受促銷而已,因?yàn)檫@部分還沒有寫進(jìn)它們的代碼里去。
- 9月——消費(fèi)者應(yīng)用和服務(wù)都進(jìn)行了有關(guān)升級(jí),以便能夠理解并使用免費(fèi)的網(wǎng)上促銷。
成功進(jìn)行演化的關(guān)鍵在于,服務(wù)的消費(fèi)者們要能夠預(yù)料到改變。在每一步,服務(wù)不是直接跟資源綁定(例如通過URI模版),而是提供指向具名資源(named resources)的URIs,以便消費(fèi)者與之交互。這些具名資源,有些是消費(fèi)者不認(rèn)識(shí)的、將被忽略的,有些是消費(fèi)者已知的、想采用的狀態(tài)遷移點(diǎn)。不管 采用哪種方式,這種方案使得服務(wù)可以優(yōu)雅地演化,同時(shí)還能維持與消費(fèi)者兼容。
你將使用的是一個(gè)相當(dāng)熱門的技術(shù)
交付咖啡是我們工作流的最后一步。我們已經(jīng)點(diǎn)了單、修改了訂單(也可能無法修改)、付過款并最終拿到了我們的咖啡。在柜臺(tái)另一側(cè),星巴克也已經(jīng)同樣完成了收款和訂單處理。
我們可以用Web來描述所有必需的交互。我們可以利用現(xiàn)有的Web模型處理一些簡單的不愉快的事(例如無法修改處理中或已處理完畢的訂單),而不必 自己發(fā)明新的異常或錯(cuò)誤處理機(jī)制——我們所需的一切都是HTTP現(xiàn)成提供的。而且,即便發(fā)生了那些不愉快的事,客戶端仍然可以向它們的目標(biāo)邁進(jìn)。
HTTP提供的特性起初看來是無關(guān)緊要的。但這個(gè)協(xié)議現(xiàn)在已經(jīng)取得廣泛的一致、并得到廣泛的部署了,而且所有的軟件與硬件都能一定程度上理解它。當(dāng) 我們看到其他分布式計(jì)算技術(shù)(如WS-*)處于割據(jù)狀態(tài)的格局時(shí),我們意識(shí)到了HTTP享有的巨大成功,以及它在系統(tǒng)間集成方面的潛力。
甚至在非功能性方面,Web也是有益的。在我們碰到臨時(shí)故障時(shí),HTTP操作(
GET
、
PUT
和
DELETE
)的冪等性質(zhì)令我們可以進(jìn)行安全的重試;內(nèi)在的緩存機(jī)制既屏蔽了故障,又有助于災(zāi)難恢復(fù)(通過增強(qiáng)的可用率);HTTPS和HTTP認(rèn)證有助于基本的安全需求。
盡管我們的問題域是人為制造的,但我們所強(qiáng)調(diào)的技術(shù)同樣可以應(yīng)用于分布式計(jì)算環(huán)境。我們不會(huì)偽稱Web很簡單(除非你是天才),Web可以解決一切問題 (除非你是超級(jí)樂觀的人,或受到REST信仰的感染),但事實(shí)上,在局部、企業(yè)級(jí)和Internet級(jí)進(jìn)行系統(tǒng)集成,Web是個(gè)健壯的框架。
致謝
本文作者要向英國卡迪夫大學(xué)(Cardiff University)的Andrew Harrison表示感謝,是他啟發(fā)了我們就Web上的“對(duì)話描述”進(jìn)行討論。
About the Authors
Jim Webber 博士是ThoughtWorks公司的專業(yè)服務(wù)主管,他的工作是為全球客戶進(jìn)行可靠的分布式系統(tǒng)架構(gòu)設(shè)計(jì)。此 前,Jim擔(dān)任英國E-Science計(jì)劃高級(jí)研究員,從事將Web服務(wù)實(shí)踐及可靠面向服務(wù)計(jì)算的架構(gòu)模式應(yīng)用于網(wǎng)格計(jì)算的戰(zhàn)略設(shè)計(jì)工作,他在Web及 Web服務(wù)架構(gòu)與 開發(fā)方面具有廣泛的經(jīng)驗(yàn)。Jim還擔(dān)任過惠普公司和Arjuna公司的架構(gòu)師,他是業(yè)界首個(gè)Web服務(wù)事務(wù)方案的首席開發(fā)者。Jim是一位活躍的演說家, 他經(jīng)常受邀出席 國際會(huì)議并發(fā)言。他還是一位活躍的作家,除了《Developing Enterprise Web Services - An Architect's Guide》這本書外,目前他正在撰寫一本關(guān)于基于Web的集成的新書。Jim獲得英國紐卡斯?fàn)柎髮W(xué)(University of Newcastle)的計(jì)算機(jī)科學(xué)學(xué)士學(xué)位和并行計(jì)算博士學(xué)位。他的博客地址是: http://jim.webber.name 。
Savas Parastatidis 是一位軟件思想家,他的思考領(lǐng)域涉及系統(tǒng)和軟件。他研究技術(shù)在eResearch里的運(yùn)用,他尤其對(duì)云計(jì)算、知識(shí)表示與管理、社會(huì)網(wǎng)絡(luò)感興趣。他目前任職于微軟研究院合作研究部。Savas喜歡在 http://savas.parastatidis.name 上寫博客。
Ian Robinson 幫助客戶們創(chuàng)建可持續(xù)的面向服務(wù)的能力,令業(yè)務(wù)與IT從開始到實(shí)際運(yùn)營始終保持齊合。他為微軟公司寫過關(guān)于采用微軟技術(shù)實(shí)現(xiàn)面向服務(wù)系統(tǒng)的指南,還發(fā)表過文章講述消費(fèi)者驅(qū)動(dòng)的服務(wù)契約及其在軟件開發(fā)生命周期中的作用——該文章可以在 《ThoughtWorks文集(The ThoughtWorks Anthology)》 (Pragmatic Programmers,2008)及 InfoQ中文站 上找到。他經(jīng)常在會(huì)議上做有關(guān)REST式企業(yè)開發(fā)及面向服務(wù)交付的測試驅(qū)動(dòng)基礎(chǔ)的講演。
1 ETag(Entity Tag的簡寫)是資源狀態(tài)的唯一標(biāo)識(shí)符。一個(gè)資源的ETag通常是根據(jù)該資源的數(shù)據(jù)得到的MD5校驗(yàn)和或SHA1哈希值。
2 我們將從稍后的星巴克例子中了解認(rèn)證的工作原理。
3 當(dāng)然,如果安全性遭到威脅,我們只要防止事情不要錯(cuò)得更厲害就行了!但得到咖啡并不是一項(xiàng)攸關(guān)安全的任務(wù),盡管每天早晨我的同事們可能會(huì)這么認(rèn)為!
4
HTTP 1.1提供了一些有用的請(qǐng)求指令,比如
max-age
、
max-stale
和
max-fresh
,它們允許客戶端指出愿意接受緩存里多舊的數(shù)據(jù)。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
