国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發(fā)設計 > 正文

WebSocket——為Web應用帶來桌面應用般的靈活性

2019-11-14 15:29:34
字體:
來源:轉載
供稿:網(wǎng)友

當今的Web應用在我們的個人生活與商業(yè)應用中的各個方面已經(jīng)表現(xiàn)出愈發(fā)重要的作用。這些應用包括社交媒體網(wǎng)絡、在線購物、商業(yè)應用,乃至家用電器的配置程序。雖然它的增長勢頭依然迅猛,但Web應用的用戶體驗與原生應用或桌面應用相比仍然相形見絀,其主要原因是Web應用的設計依賴于單向的HTTP協(xié)議。而WebSocket將改變這一現(xiàn)狀,它為瀏覽器與服務端的交互帶來了一種新的基礎元素,為創(chuàng)建一種能夠提供真正的交互性體驗的Web應用提供了必要的基礎。

早期的Web技術都是基于HTTP協(xié)議而發(fā)展起來的,而HTTP只是一個簡單的基于請求 —— 響應操作的協(xié)議,所有的請求都是由客戶端發(fā)起的。這套框架原本足以滿足用戶的需求,但在如今開發(fā)者所設計的web應用中,由客戶端發(fā)起通信這種方式有著很大的制約。雖然人們提出了各種臨時方案,但它們都是基于HTTP協(xié)議的,只是應用了輪詢或長輪詢技術(例如Comet)。Comet能夠讓負責處理請求的線程得到釋放,以防止服務器資源耗盡。由于輪詢這種機制并不可靠,因此在2007年時,有人提出了一種名為WebSocket的全雙工(full-duplex)類型的通信方式。這項提議用了整整4年的時間才成為一個標準。但是,盡管它已成為一種標準,但它的使用率卻相當有限。本文將為讀者解釋妨礙WebSocket應用的兩大原因,并且提出了一個設計框架,開發(fā)者可以使用這套框架快速地發(fā)揮WebSocket的潛能,并且極大地豐富應用的體驗。

導致WebSocket使用率低下的第一個原因在于應用服務器與瀏覽器對其支持不足。但隨著新一代應用服務器與瀏覽器的出現(xiàn),這種狀況得到了很大的改善。而第二個原因比起前一點來說其影響更大,亦即要充分利用WebSocket的全部潛能,必須對Web應用進行顛覆性的重新設計。而這個重新設計過程需要將基礎的請求 —— 響應這一結構轉變?yōu)楦鼜碗s的雙向消息傳遞結構。應用程序的重新設計往往是一個開銷很大的過程,而軟件供應商很難從這一過程中看到任何顯著的利益。

我們首先將對WebSocket做個簡單的介紹,隨后展示一種使用WebSocket重新構建應用程序的方法,最后通過一個簡單的示例表現(xiàn)這一方法的各種要點。

WebSocket簡介

WebSocket是在TCP/ip協(xié)議之上創(chuàng)建的一種幀協(xié)議,客戶端將通過向服務器發(fā)送一種特殊的HTTP請求以啟動WebSocket。在最初的握手過程之后,客戶端與服務器就能夠自由地以異步方式互相進行幀的傳送了。幀分為兩種類型:即控制幀與數(shù)據(jù)幀。最小的控制幀僅有2比特的大小,而在數(shù)據(jù)幀方面,客戶端的數(shù)據(jù)幀最小為6比特,服務端的數(shù)據(jù)幀最小為2比特。數(shù)據(jù)幀既可以是文本型,也可以是二進制的。文本幀都經(jīng)過了UTF-8的編碼。幀可以實現(xiàn)分塊,因此一個大數(shù)據(jù)集可以分解為多個幀。WebSocket不會為幀附加任何標識信息,因此不同類型的信息對應的幀不可混用。只有控制幀能夠在處理一個大消息時的一系列中間幀中出現(xiàn)。在這些基礎的幀之上,還可以定義更復雜的協(xié)議。比方說,一個幀能夠帶有校驗和或是它的序列號等相關信息。

WebSocket的API

WebSocket并不限定于僅在某個特定的編程語言、系統(tǒng)或是操作系統(tǒng)中使用。多數(shù)主流的編程語言以及許多瀏覽器都已開始支持WebSocket的編程。雖然在不同的平臺與編程語言中存在著大量的標準,但本文僅關注javaScript HTML5以及Java(J2EE)對WebSocket的支持。在瀏覽器這方面有兩種實現(xiàn)標準,其最新版本分別為Hixie-76和HyBi-17(不久之后發(fā)展為IETF RFC 6455)。HyBi的實現(xiàn)相對更高級,并且得到了目前所有主流瀏覽器的支持。而在服務端方面,基于Java的實現(xiàn)則是目前最為流行的。早些時候在Java上曾經(jīng)出現(xiàn)過幾種WebSocket的實現(xiàn),它們之后已發(fā)展為JSR 356這種實現(xiàn)。JSR代表Java規(guī)范請求,對規(guī)范請求的說明有幫于讓之后的各種實現(xiàn)保持一致性,并且易于使用。JSR也讓開發(fā)者不必依賴于某個特定的實現(xiàn)。JSR 356與servlet規(guī)范是相互分離的,但它也允許開發(fā)者訪問某些servlet對象。JSR 356的內(nèi)容涵蓋了WebSocket連接的客戶端與服務端, 我們稍后的討論將集中于配合瀏覽器端的Javascript所實現(xiàn)的服務端。JSR 356目前屬于J2EE 7的一部分,所有流行的開源Java應用服務器都支持它,包括Tomcat、Jetty、Glassfish以及TJWS等等。除此之外,在Java環(huán)境中還存在著大約20種各自獨立的WebSocket服務端解決方案,其中有些方案也支持JSR 356。由于WebSocket是J2EE 7的一部分,因而在由Oracle與IBM所推出的商業(yè)應用服務器上同樣也得到支持。

正如我之前所說,WebSocket是一種消息傳遞協(xié)議。它的API提供了各種在通信雙方進行消息傳遞與接收的方法。這里并不存在經(jīng)典的訂閱者與發(fā)布者的關系。消息只有兩種類型,即文本型與二進制型。不過,在這些類型的消息處理函數(shù)中可以對消息進行邏輯上的分離。在Java中能夠以某種方式處理被分解為多個塊的部分消息,而JavaScript尚未支持這種程度的控制能力。如同之前所說,WebSocket是一種非常泛用的協(xié)議,它可以在握手時指定所需的邏輯子協(xié)議。當不同的系統(tǒng)能夠驗證所連到的系統(tǒng)支持這種邏輯子協(xié)議及擴展時,使用WebSocket進行系統(tǒng)集成就變得容易很多。WebSocket幀格式允許在它的基礎上使用可協(xié)商的擴展,這與意味著一般來說幀可能會提供更多的信息,并且可能會引入不同的幀類型。

瀏覽器端的JavaScript實現(xiàn)

由于WebSocket協(xié)議的握手過程是由客戶端發(fā)起的,因此需要通過包含了WebSocket接口的JavaScript代碼對所有WebSocket操作進行封裝。

該接口已經(jīng)實現(xiàn)了標準化1,并通過接口定義語言(IDL)進行定義,如以下代碼所示:

[Constructor(in DOMString url, in optional DOMString PRotocols)][Constructor(in DOMString url, in optional DOMString[] protocols)]interface WebSocket {  readonly attribute DOMString url;  // ready state  const unsigned short CONNECTING = 0;  const unsigned short OPEN = 1;  const unsigned short CLOSING = 2;  const unsigned short CLOSED = 3;  readonly attribute unsigned short readyState;  readonly attribute unsigned long bufferedAmount;  // networking           attribute Function onopen;           attribute Function onmessage;           attribute Function onerror;           attribute Function onclose;  readonly attribute DOMString protocol;  void send(in DOMString data);  void close();};WebSocket implements EventTarget;

WebSocket的構建函數(shù)包含兩個參數(shù):

  1. WebSocket的URL
  2. 必要的子協(xié)議的數(shù)組或單個元素,這一參數(shù)是可選的

WebSocket的URL都是以“ws”為前兩個字符,它代表所使用的是WebSocket協(xié)議,而其余部分與HTTP協(xié)議中的URL相同,包括主機、端口、路徑以及查詢字符串。如果需要使用安全連接,可以在協(xié)議名稱上加一個額外的“s”字符。

可以指定的消息處理函數(shù)共有四種:onopen、onmessage、onclose和onerror。在傳遞消息時需要調(diào)用send方法,而在關閉連接時則需要調(diào)用close方法。由于不存在類似于connect這樣的方法,因此客戶端必須監(jiān)聽onopen消息,以確認連接已建立,隨后才能夠進行send操作。另一種選擇是對WebSocket對象的readyState屬性進行輪詢,但這種方式并不推薦使用。顯然,在onmessage處理函數(shù)中總是能夠調(diào)用send操作的。send操作由客戶端異步執(zhí)行,這也意味著JavaScript在將消息傳遞給接收者的過程中無須等待其結果,而是直接返回。文本消息或二進制消息在接收時不存在任何差別,因此在onmessage處理函數(shù)中必須對事件的data參數(shù)進行檢查。WebSocket提供了一些屬性,可用于獲取狀態(tài)、判斷二進制消息的格式等目的。而其它瀏覽器廠商的特定實現(xiàn)中還可以包含更多的屬性,因此請記得仔細閱讀瀏覽器的文檔,以了解詳細的信息。

Java端的WebSocket實現(xiàn)

Java中的JSR 356定義了常見的(客戶端)與服務端的Java WebSocket通信API。在Java的實現(xiàn)中會指定終結點與服務端終結點對象,這與JavaScript中的WebSocket實現(xiàn)頗為類似。可以通過注解的方式將某個Java類指定為一個終結點對象,而通過OnOpen、OnMessage、OnError和 OnClose等注解信息指定事件處理函數(shù)。在每種類型的處理函數(shù)中,都可以將重要的session對象作為一個傳入?yún)?shù)。Session對象讓開發(fā)者能夠訪問發(fā)送消息的功能,并且能夠保持與WebSocket連接相關的狀態(tài)特性。消息的發(fā)送可以使用同步或異步機制,并且在兩種類型的發(fā)送機制中都可以指定超時時間。通過指定相應的解碼器,二進制與文本數(shù)據(jù)都能夠自動轉換為任意的Java對象,而編碼器則允許WebSocket發(fā)送任意類型的Java對象。對于某個特定的WebSocket URL路徑,消息處理函數(shù)只能對應文本消息類型或二進制消息類型的其中一種。Java中未提供消息鏈的功能,但也可以通過編程的方式對其進行組織。Java端的API很容易上手,它提供了一種可自定義的配置對象,能夠影響最初的握手過程,決定所支持的子協(xié)議、版本,并且提供訪問重要的servlet對象API的功能。終結點不僅能夠通過注解的方式進行部署,也能夠通過編程的方式所生成。

對Web應用的重新思考

WebSocket對于以下類型的應用程序的開發(fā)是一種非常自然的選擇:

  1. 需要玩家之間實時協(xié)作的游戲
  2. 實時監(jiān)控系統(tǒng)
  3. 需要用戶進行協(xié)作的系統(tǒng),例如聊天、共享文檔的編輯等等。

其實,WebSocket在傳統(tǒng)的Web應用中也能夠展現(xiàn)其優(yōu)勢。大多數(shù)Web應用都是基于請求 - 響應這一范式進行設計的。雖然Ajax能夠實現(xiàn)異步操作,但在繼續(xù)處理下一步操作之間,仍然必須等待響應返回。而由于WebSocket連接只需建立一次,從而避免了為每次數(shù)據(jù)交換重建連接的過程,并且在后續(xù)的通信中也無需發(fā)送多余的HTTP頭信息。這種優(yōu)勢在SSL類型的連接上體現(xiàn)得尤為明顯,因為最初的連接握手是一個開銷很大的操作。瀏覽器端的WebSocket發(fā)送操作是完全異步的,而Java的服務端代碼在發(fā)送消息后無需進行等待。由于發(fā)送消息的這種自由度,在應用中或許需要對某些操作進行手動記錄,以保持應用狀態(tài)的一致性。在使用WebSocket時也能夠模擬請求 - 響應這一范式,但如此一來,WebSocket作為一種真正的異步雙向消息傳遞系統(tǒng)的優(yōu)勢也被大大消減了。由于以上所描述的這些特性,因此應當鼓勵開發(fā)者在某些場景中對應用程序的設計方式進行重新思考。

假設某一個應用程序包含了復雜的用戶界面,其中某些區(qū)域的功能需要通過服務端的大量計算才能夠生成對應的內(nèi)容。傳統(tǒng)的基于AJAX的實現(xiàn)方式可以選擇一種延遲調(diào)用的機制,通過某個內(nèi)容請求調(diào)用以生成這一區(qū)域的內(nèi)容。而在使用WebSocket的場合下,服務端可以在瀏覽器做好準備的情況下直接發(fā)送內(nèi)容,而無需對某個AJAX請求進行響應。AJAX請求這一方式的缺陷在于,由于瀏覽器所發(fā)送的請求是串行的,因此服務端的處理過程無法針對請求的順序進行相應的優(yōu)化。而WebSocket為服務端提供了一個自行決定最佳的內(nèi)容生成方式的機會,因而能夠提升Web應用的整體響應性。

要用效地利用WebSocket的功能,還需要仔細考慮幾個額外的要點。由于在WebSocket中隨時可能出現(xiàn)網(wǎng)絡連接的丟失,使數(shù)據(jù)無法正確地傳遞,因此對于一些至關重要的數(shù)據(jù)需要進行一些額外的手動記錄操作。一般來說,所收到的每條消息都必須提供足夠的信息,以指示如何對其進行處理。但沒有有效的手段能夠了解信息的請求者是誰,是來自客戶端的請求,還是說服務端想要更新某些內(nèi)容。在具體使用WebSocket的過程中,可能需要對Web應用的設計進行更深入的重新思考。此外,JavaScript代碼的功能可以遷移至服務端,打個比方,用戶的輸入可以立即發(fā)送給服務端進行處理,通過這種方式能夠實現(xiàn)一些復雜的數(shù)據(jù)校驗操作,而這些校驗功能或許是JavaScript所無法處理的。用戶的輸入還能夠即時地保存在后臺系統(tǒng)中,因此瀏覽器就無需將最終的數(shù)據(jù)傳遞給服務器進行額外的數(shù)據(jù)校驗,因為數(shù)據(jù)在保存在后臺期間已經(jīng)經(jīng)過了校驗。如果要使某個應用從富Web客戶端轉為一種輕量級的客戶端,就可以考慮以這種方式增加服務端代碼的職責。

使用WebSocket時所需注意的要點

在Web應用開發(fā)時使用WebSocket也會面對一些特別的挑戰(zhàn),WebSocket的Session與HTTP的Session之間并無任何關聯(lián),雖然也可將其用作類似的目標。在Session中可以附加某些通用的數(shù)據(jù),因此所有的消息處理過程都可以依賴于Session中所維護的某些狀態(tài)和數(shù)據(jù)。WebSocket的Session也可以根據(jù)空閑(不活躍)時間間隔的配置產(chǎn)生超時情況,正如HTTP Session一樣。不過有些系統(tǒng)會自動地持續(xù)發(fā)送Ping這一控制消息,以防止出現(xiàn)超時。JSR 356建議將HTTP Session與WebSocket Session的超時進行同步。一旦HTTP Session超時,在其范圍內(nèi)所創(chuàng)建的所有WebSocket連接也都必須關閉。但有些Web應用的設計不會產(chǎn)生任何HTTP Session,而有些應用的Session超時不依賴于HTTP Session,而是由JavaScript所管理的,因此這種機制并不能夠進行可靠的推廣。

另一種需要注意的要點在于,某些瀏覽器會維護一個連接池,以重用連接的方式訪問相同的網(wǎng)站,因此這種流程可以被串行化。而如果瀏覽器為WebSocket連接也創(chuàng)建一個連接池,那么它會受到嚴重的制約。因為如果沒有某種機制保持WebSocket連接的關閉,這個連接就永遠處于活躍狀態(tài),而其它任何創(chuàng)建新連接的嘗試都會產(chǎn)生死鎖。因此,最佳實踐的推薦做法是只使用一個WebSocket連接。

瀏覽器無法對通過WebSocket進行傳遞的數(shù)據(jù)進行緩存,因此通過WebSocket傳遞可以在瀏覽器中緩存的資源
(例如圖片、CSS等)并非一種有效的途徑。

WebSocket與RESTful的比較

在網(wǎng)絡上對于RESTful與WebSocket之間的討論從未停歇2。不過,這些比較中的大部分都不是在一個層面上的比較,好比關公戰(zhàn)秦瓊。REST是指表述性狀態(tài)轉換,多數(shù)情況下它需要依賴底層的HTTP協(xié)議實現(xiàn),也就是說REST是一個基于請求 - 響應的協(xié)議。REST這種風格沒有經(jīng)過標準化,因此任何一種通過HTTP進行通信的方式在某些范圍內(nèi)都可以稱為REST。REST通常會將新增、讀取、更新和刪除操作(CRUD)與對應的HTTP方法PUT、GET、DELETE之間建立映射關系。而WebSocket所處理的是消息,因此對于單一的RPC來說不存在一個確定的范圍。REST的通信數(shù)據(jù)格式通常僅限JSON格式以及請求參數(shù),而一個WebSocket消息體可以表現(xiàn)為任何類型,包括純粹的二進制數(shù)據(jù)3

當然,WebSocket也能夠用于與REST相似的目的,但在大多數(shù)情況下,這種做法有些刻意為之了。正如上文所述,在使用WebSocket過程中需要應用一些不同的設計原則。下表描述了這兩者之間的主要區(qū)別4

 

文件上傳的示例

以下示例展現(xiàn)了如何通過使用WebSocket將一個文件上傳至服務器,首先最好定義一個服務端的終結點。

@ServerEndpoint("/upload/{file}")public class UploadServer {

其中要定義兩個消息處理函數(shù),一個用于接收上傳文件的二進制數(shù)據(jù),而另一個則用于命令接口。由于在WebSocket中允許分離文本與二進制消息,因此在定義兩個處理函數(shù)時無需進行額外的操作。用于接收命令的OnMessage處理函數(shù)定義如下:

@OnMessage    public void processCmd(CMD cmd, Session ses) {
CMD類的定義如下
static class CMD {        public int cmd;        public String data;    }

為了將文本消息轉換為CMD對象,需要指定一個解碼器,其定義如下:

public static class CmdDecoder implements Decoder.Text<CMD> {

將文本信息編碼為JSON格式并不是一種強制性的要求,只是在這個示例中需要用到JSON。大文件的上傳是分多個塊進行的,以減少內(nèi)存的開銷。在瀏覽器中無法利用WebSocket的部分幀,因此需要用到完整的幀來模擬塊傳送。由于瀏覽器以異步的方式發(fā)送所有的消息,因此無法得知服務端是否已經(jīng)接收到了一個完整的文件。命令接口的作用是完成以下工作:

  1. 通知服務器上傳即將開始,并且為上傳文件設定一個名稱
  2. 通知服務器已上傳了一個完整的文件
  3. 向客戶端發(fā)送確認,表示文件已成功地保存了

同樣的CMD對象可以進行重用,以滿足各種需求。傳入的命令是按照以下方式進行處理的:

@OnMessage    public void processCmd(CMD cmd, Session ses) {        switch (cmd.cmd) {        case 1: // start            fileName = cmd.data;            break;        case 2: // finish            close(ses);            cmd.cmd = 3;            ses.getAsyncRemote().sendObject(cmd);            break;        }    }

這種實現(xiàn)方式假設瀏覽器端會將所有發(fā)送消息的活動進行串行化,即所有消息的到達順序與發(fā)送順序是一致的。但是如果某個客戶端使用了某些并行方式進行發(fā)送,那么就需要一種更為復雜的實現(xiàn)方式,讓每個所發(fā)送的消息都帶有一個ID。另一種方案是為每個收到的文件塊都發(fā)送一次確認消息,只是這樣一來WebSocket的優(yōu)勢也就喪失殆盡了。由于CMD對象的目標是將消息發(fā)送至客戶端,因此必須提供一個編碼器:

public static class CmdEncoder implements Encoder.Text<CMD> {

在ServerEndpoint的注解中必須指定解碼器與編碼器信息,如下所示:

@ServerEndpoint(value = "/upload/{file}", decoders = UploadServer.CmdDecoder.class, encoders=UploadServer.CmdEncoder.class)public class UploadServer {

二進制消息的處理函數(shù)定義如下:

@OnMessagepublic void savePart(byte[] part, Session ses) {    if (uploadFile == null) {        if (fileName != null)            try {                uploadFile = new RandomaccessFile(fileName, "rw");            } catch (FileNotFoundException e) {                // TODO Auto-generated catch block                e.printStackTrace();                return;            }    }    if (uploadFile != null)    try {        uploadFile.write(part);        System.err.printf("Stored part of %db%n", part.length);    } catch (IOException e) {        // TODO Auto-generated catch block        e.printStackTrace();    }}

此外還可以為OnClose事件加入一個處理函數(shù),萬一出現(xiàn)連接異常關閉的情況,它將負責刪除不完整的文件。

客戶端的實現(xiàn)利用了HTML5中的工作線程(Worker)功能,不幸的是,Firefox沒有采用在Worker中實現(xiàn)文件對象克隆的方式,因此這個示例只能在IE或Chrome中進行測試。如果該解決方案對于瀏覽器的可移植性有很高的要求,那么可以用一個不使用Worker的JavaScript代碼段來代替這個基于Worker的解決方案。但由于未使用獨立的線程(即Worker),因此這種方案的性能會有所下降。Worker的代碼如下所示:

var files = [];var endPoint = "ws" + (self.location.protocol == "https:" ? "s" : "") + "://"        + self.location.hostname        + (self.location.port ? ":" + self.location.port : "")        + "/echoserver/upload/*";var socket;var ready;function upload(blobOrFile) {    if (ready)        socket.send(blobOrFile);}function openSocket() {    socket = new WebSocket(endPoint);    socket.onmessage = function(event) {        self.postMessage(JSON.parse(event.data));    };    socket.onclose = function(event) {        ready = false;    };    socket.onopen = function() {        ready = true;        process();    };}function process() {    while (files.length > 0) {        var blob = files.shift();        socket.send(JSON.stringify({            "cmd" : 1,            "data" : blob.name        }));        const        BYTES_PER_CHUNK = 1024 * 1024 * 2;        // 1MB chunk sizes.        const        SIZE = blob.size;        var start = 0;        var end = BYTES_PER_CHUNK;        while (start < SIZE) {            if ('mozSlice' in blob) {                var chunk = blob.mozSlice(start, end);            } else if ('slice' in blob) {                    var chunk = blob.slice(start, end);            } else {                var chunk = blob.webkitSlice(start, end);            }            upload(chunk);            start = end;            end = start + BYTES_PER_CHUNK;        }        socket.send(JSON.stringify({            "cmd" : 2,            "data" : blob.name        }));        //self.postMessage(blob.name + " Uploaded Succesfully");    }}self.onmessage = function(e) {    for (var j = 0; j < e.data.files.length; j++)        files.push(e.data.files[j]);    //self.postMessage("Job size: "+files.length);    if (ready) {        process();    } else        openSocket();}

很方便的一點在于,與Worker進行交互的JavaScript代碼也能夠利用消息傳遞機制。當用戶在瀏覽器中選擇文件進行上傳時,這一操作的信息就會傳遞給Worker。后者會以批量的方式處理第一個準備上傳的文件,它將文件分成多個片段,即多個塊,然后通過WebSocket將這些塊依次上傳。最后發(fā)送一個cmd = 2的命令消息。而命令消息的處理函數(shù)會將消息重新發(fā)送給主JavaScript代碼,通知所上傳的文件已經(jīng)完成了。如果客戶端選擇上傳許多大文件,那么這段代碼會對瀏覽器端帶來相當大的壓力。為此需要對代碼進行重新調(diào)整,讓它在收到上一個文件上傳成功的消息后才繼續(xù)上傳下一個文件。這部分內(nèi)容的修改就留給各位讀者作為一個練習吧。在附錄1中可以找到本示例的完整源代碼。

全能程序員交流QQ群290551701,群內(nèi)程序員都是來自,百度、阿里、京東、小米、去哪兒、餓了嗎、藍港等高級程序員 ,擁有豐富的經(jīng)驗。加入我們,直線溝通技術大牛,最佳的學習環(huán)境,了解業(yè)內(nèi)的一手的資訊。如果你想結實大牛,那 就加入進來,讓大牛帶你超神!


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 大邑县| 阜宁县| 莎车县| 大庆市| 丰镇市| 云南省| 淮阳县| 旬邑县| 廉江市| 太保市| 揭西县| 扎赉特旗| 贵港市| 彰武县| 吉木乃县| 奎屯市| 喜德县| 张家界市| 兴业县| 象州县| 高阳县| 桂阳县| 洪泽县| 鹰潭市| 楚雄市| 固始县| 阳江市| 浮山县| 米林县| 河间市| 抚宁县| 南京市| 马关县| 资兴市| 丘北县| 普陀区| 平果县| 库尔勒市| 黔西县| 新平| 突泉县|