91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux高性能服務器架構設計方法是什么

發布時間:2022-01-19 15:09:21 來源:億速云 閱讀:136 作者:iii 欄目:服務器

本篇內容主要講解“Linux高性能服務器架構設計方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Linux高性能服務器架構設計方法是什么”吧!

一、框架篇

我們先從單個服務程序的組織結構開始介紹。

(一)、網絡通信

既然是服務器程序肯定會涉及到網絡通信部分,那么服務器程序的網絡通信模塊要解決哪些問題?

筆者認為至少要解決以下問題:

1. 如何檢測有新客戶端連接?

2. 如何接受客戶端連接?

3. 如何檢測客戶端是否有數據發來?

4.如何收取客戶端發來的數據?

5.如何檢測連接異常?發現連接異常之后,如何處理?

6.如何給客戶端發送數據?

7.如何在給客戶端發完數據后關閉連接?

稍微有點網絡基礎的人,都能回答上面說的其中幾個問題,比如接收客戶端連接用socket API的accept函數,收取客戶端數據用recv函數,給客戶端發送數據用send函數,檢測客戶端是否有新連接和客戶端是否有新數據可以用IO multiplexing技術(IO復用)的select、poll、epoll等socket API。確實是這樣的,這些基礎的socket API構成了服務器網絡通信的地基,不管網絡通信框架設計的如何巧妙,都是在這些基礎的socket API的基礎上構建的。但是如何巧妙地組織這些基礎的socket API,才是問題的關鍵。我們說服務器很高效,支持高并發,實際上只是一個技術實現手段,不管怎樣從軟件開發的角度來講無非就是一個程序而已,所以,只要程序能最大可能地滿足“盡量減少等待”就是高效。也就是說高效不是“忙的忙死,閑的閑死”,而是大家都可以閑著,但是如果有活要干,大家盡量一起干,而不是一部分忙著依次做事情123456789,另外一部分閑在那里無所事事。說的可能有點抽象,下面我們來舉一些例子具體來說明一下。

比如默認recv函數如果沒有數據的時候,線程就會阻塞在那里;

默認send函數,如果tcp窗口不是足夠大,數據發不出去也會阻塞在那里;

connect函數默認連接另外一端的時候,也會阻塞在那里;

又或者是給對端發送一份數據,需要等待對端回答,如果對方一直不應答,當前線程就阻塞在這里。

以上都不是高效服務器的開發思維方式,因為上面的例子都不滿足“盡量減少等待”的原則,為什么一定要等待呢?有沒用一種方法,這些過程不需要等待,最好是不僅不需要等待,而且這些事情完成之后能通知我。這樣在這些本來用于等待的cpu時間片內,我就可以做一些其他的事情。有,也就是我們下文要討論的IO Multiplexing技術(IO復用技術)。

(二)、幾種IO復用機制的比較

目前windows系統支持select、WSAAsyncSelect、WSAEventSelect、完成端口(IOCP),linux系統支持select、poll、epoll。這里我們不具體介紹每個具體的函數的用法,我們來討論一點深層次的東西,以上列舉的API函數可以分為兩個層次:

層次一 select和poll

層次二 WSAAsyncSelect、WSAEventSelect、完成端口(IOCP)、epoll

為什么這么分呢?先來介紹第一層次,select和poll函數本質上還是在一定時間內主動去查詢socket句柄(可能是一個也可能是多個)上是否有事件,比如可讀事件,可寫事件或者出錯事件,也就是說我們還是需要每隔一段時間內去主動去做這些檢測,如果在這段時間內檢測出一些事件來,我們這段時間就算沒白花,但是倘若這段時間內沒有事件呢?我們只能是做無用功了,說白了,還是在浪費時間,因為假如一個服務器有多個連接,在cpu時間片有限的情況下,我們花費了一定的時間檢測了一部分socket連接,卻發現它們什么事件都沒有,而在這段時間內我們卻有一些事情需要處理,那我們為什么要花時間去做這個檢測呢?把這個時間用在做我們需要做的事情不好嗎?所以對于服務器程序來說,要想高效,我們應該盡量避免花費時間主動去查詢一些socket是否有事件,而是等這些socket有事件的時候告訴我們去處理。這也就是層次二的各個函數做的事情,它們實際相當于變主動查詢是否有事件為當有事件時,系統會告訴我們,此時我們再去處理,也就是“好鋼用在刀刃”上了。只不過層次二的函數通知我們的方式是各不相同,比如WSAAsyncSelect是利用windows消息隊列的事件機制來通知我們設定的窗口過程函數,IOCP是利用GetQueuedCompletionStatus返回正確的狀態,epoll是epoll_wait函數返回而已。

比如connect函數連接另外一端,如果連接socket是異步的,那么connect雖然不能立刻連接完成,但是也是會立刻返回,無需等待,等連接完成之后,WSAAsyncSelect會返回FD_CONNECT事件告訴我們連接成功,epoll會產生EPOLLOUT事件,我們也能知道連接完成。甚至socket有數據可讀時,WSAAsyncSelect產生FD_READ事件,epoll產生EPOLLIN事件,等等。所以有了上面的討論,我們就可以得到網絡通信檢測可讀可寫或者出錯事件的正確姿勢。這是我這里提出的第二個原則:盡量減少做無用功的時間。這個在服務程序資源夠用的情況下可能體現不出來什么優勢,但是如果有大量的任務要處理,個人覺得這個可能帶來無用

(三)、檢測網絡事件的正確姿勢

根據上面的介紹,第一,為了避免無意義的等待時間,第二,不采用主動查詢各個socket的事件,而是采用等待操作系統通知我們有事件的狀態的策略。我們的socket都要設置成異步的。在此基礎上我們回到欄目(一)中提到的七個問題:

1. 如何檢測有新客戶端連接?

2. 如何接受客戶端連接?

默認accept函數會阻塞在那里,如果epoll檢測到偵聽socket上有EPOLLIN事件,或者WSAAsyncSelect檢測到有FD_ACCEPT事件,那么就表明此時有新連接到來,這個時候調用accept函數,就不會阻塞了。當然產生的新socket你應該也設置成非阻塞的。這樣我們就能在新socket上收發數據了。

3. 如何檢測客戶端是否有數據發來?

4.如何收取客戶端發來的數據?

同理,我們也應該在socket上有可讀事件的時候才去收取數據,這樣我們調用recv或者read函數時不用等待,至于一次性收多少數據好呢?我們可以根據自己的需求來決定,甚至你可以在一個循環里面反復recv或者read,對于非阻塞模式的socket,如果沒有數據了,recv或者read也會立刻返回,錯誤碼EWOULDBLOCK會表明當前已經沒有數據了。示例:

bool CIUSocket::Recv()  {      int nRet = 0;       while(true)      {          char buff[512];          nRet = ::recv(m_hSocket, buff, 512, 0);      if(nRet == SOCKET_ERROR)                //一旦出現錯誤就立刻關閉Socket      {          if (::WSAGetLastError() == WSAEWOULDBLOCK)            break;          else              return false;      }      else if(nRet < 1)          return false;           m_strRecvBuf.append(buff, nRet);           ::Sleep(1);      }       return true;  }

5.如何檢測連接異常?發現連接異常之后,如何處理?

同樣當我們收到異常事件后例如EPOLLERR或關閉事件FD_CLOSE,我們就知道了有異常產生,我們對異常的處理一般就是關閉對應的socket。另外,如果send/recv或者read/write函數對一個socket進行操作時,如果返回0,那說明對端已經關閉了socket,此時這路連接也沒必要存在了,我們也可以關閉對應的socket。

6.如何給客戶端發送數據?

給客戶端發送數據,比收數據要稍微麻煩一點,也是需要講點技巧的。首先我們不能像檢測數據可讀一樣檢測數據可寫,因為如果檢測可寫的話,一般情況下只要對端正常收取數據,我們的socket就都是可寫的,如果我們設置監聽可寫事件,會導致頻繁地觸發可寫事件,但是我們此時并不一定有數據需要發送。所以正確的做法是:如果有數據要發送,則先嘗試著去發送,如果發送不了或者只發送出去部分,剩下的我們需要將其緩存起來,然后設置檢測該socket上可寫事件,下次可寫事件產生時,再繼續發送,如果還是不能完全發出去,則繼續設置偵聽可寫事件,如此往復,一直到所有數據都發出去為止。一旦所有數據都發出去以后,我們要移除偵聽可寫事件,避免無用的可寫事件通知。不知道你注意到沒有,如果某次只發出去部分數據,剩下的數據應該暫且存起來,這個時候我們就需要一個緩沖區來存放這部分數據,這個緩沖區我們稱為“發送緩沖區”。發送緩沖區不僅存放本次沒有發完的數據,還用來存放在發送過程中,上層又傳來的新的需要發送的數據。為了保證順序,新的數據應該追加在當前剩下的數據的后面,發送的時候從發送緩沖區的頭部開始發送。也就是說先來的先發送,后來的后發送。

7.如何在給客戶端發完數據后關閉連接?

這個問題比較難處理,因為這里的“發送完”不一定是真正的發送完,我們調用send或者write函數即使成功,也只是向操作系統的協議棧里面成功寫入數據,至于能否被發出去、何時被發出去很難判斷,發出去對方是否收到就更難判斷了。所以,我們目前只能簡單地認為send或者write返回我們發出數據的字節數大小,我們就認為“發完數據”了。然后調用close等socket API關閉連接。關閉連接的話題,我們再單獨開一個小的標題來專門討論一下。

(四)被動關閉連接和主動關閉連接

在實際的應用中,被動關閉連接是由于我們檢測到了連接的異常事件,比如EPOLLERR,或者對端關閉連接,send或recv返回0,這個時候這路連接已經沒有存在必要的意義了,我們被迫關閉連接。

而主動關閉連接,是我們主動調用close/closesocket來關閉連接。比如客戶端給我們發送非法的數據,比如一些網絡攻擊的嘗試性數據包。這個時候出于安全考慮,我們關閉socket連接。

(五)發送緩沖區和接收緩沖區

上面已經介紹了發送緩沖區了,并說明了其存在的意義。接收緩沖區也是一樣的道理,當收到數據以后,我們可以直接進行解包,但是這樣并不好,理由一:除非一些約定俗稱的協議格式,比如http協議,大多數服務器的業務的協議都是不同的,也就是說一個數據包里面的數據格式的解讀應該是業務層的事情,和網絡通信層應該解耦,為了網絡層更加通用,我們無法知道上層協議長成什么樣子,因為不同的協議格式是不一樣的,它們與具體的業務有關。理由二:即使知道協議格式,我們在網絡層進行解包處理對應的業務,如果這個業務處理比較耗時,比如讀取磁盤文件,或者連接數據庫進行賬號密碼驗證,那么我們的網絡線程會需要大量時間來處理這些任務,這樣其它網絡事件可能沒法及時處理。鑒于以上二點,我們確實需要一個接收緩沖區,將收取到的數據放到該緩沖區里面去,并由專門的業務線程或者業務邏輯去從接收緩沖區中取出數據,并解包處理業務。

說了這么多,那發送緩沖區和接收緩沖區該設計成多大的容量?這是一個老生常談的問題了,因為我們經常遇到這樣的問題:預分配的內存太小不夠用,太大的話可能會造成浪費。怎么辦呢?答案就是像string、vector一樣,設計出一個可以動態增長的緩沖區,按需分配,不夠還可以擴展。

需要特別注意的是,這里說的發送緩沖區和接收緩沖區是每一個socket連接都存在一個。這是我們最常見的設計方案。

(六)協議的設計

除了一些通用的協議,如http、ftp協議以外,大多數服務器協議都是根據業務制定的。協議設計好了,數據包的格式就根據協議來設置。我們知道tcp/ip協議是流式數據,所以流式數據就是像流水一樣,數據包與數據包之間沒有明顯的界限。比如A端給B端連續發了三個數據包,每個數據包都是50個字節,B端可能先收到10個字節,再收到140個字節;或者先收到20個字節,再收到20個字節,再收到110個字節;也可能一次性收到150個字節。這150個字節可以以任何字節數目組合和次數被B收到。所以我們討論協議的設計第一個問題就是如何界定包的界線,也就是接收端如何知道每個包數據的大小。目前常用有如下三種方法:

固定大小,這種方法就是假定每一個包的大小都是固定字節數目,比如上文中討論的每個包大小都是50個字節,接收端每收氣50個字節就當成一個包;

指定包結束符,比如以一個\r\n(換行符和回車符)結束,這樣對端只要收到這樣的結束符,就可以認為收到了一個包,接下來的數據是下一個包的內容;

指定包的大小,這種方法結合了上述兩種方法,一般包頭是固定大小,包頭中有一個字段指定包體或者整個大的大小,對端收到數據以后先解析包頭中的字段得到包體或者整個包的大小,然后根據這個大小去界定數據的界線。

協議要討論的第二個問題是,設計協議的時候要盡量方便解包,也就是說協議的格式字段應該盡量清晰明了。

協議要討論的第三個問題是,根據協議組裝的數據包應該盡量小,這樣有如下好處:第一、對于一些移動端設備來說,其數據處理能力和帶寬能力有限,小的數據不僅能加快處理速度,同時節省大量流量費用;第二、如果單個數據包足夠小的話,對頻繁進行網絡通信的服務器端來說,可以大大減小其帶寬壓力,其所在的系統也能使用更少的內存。試想:假如一個股票服務器,如果一只股票的數據包是100個字節或者1000個字節,那100只股票和10000只股票區別呢?

協議要討論的第二個問題是,對于數值類型,我們應該顯式地指定數值的長度,比如long型,如果在32位機器上是32位的4個字節,但是如果在64位機器上,就變成了64位8個字節了。這樣同樣是一個long型,發送方和接收方可能會用不同的長度去解碼。所以建議最好,在涉及到跨平臺使用的協議最好顯式地指定協議中整型字段的長度,比如int32,int64等等。下面是一個協議的接口的例子:

class BinaryReadStream  {      private:          const char* const ptr;          const size_t      len;          const char*      cur;          BinaryReadStream(const BinaryReadStream&);          BinaryReadStream& operator=(const BinaryReadStream&);       public:          BinaryReadStream(const char* ptr, size_t len);          virtual const char* GetData() const;          virtual size_t GetSize() const;          bool IsEmpty() const;          bool ReadString(string* str, size_t maxlen, size_t& outlen);          bool ReadCString(char* str, size_t strlen, size_t& len);          bool ReadCCString(const char** str, size_t maxlen, size_t& outlen);          bool ReadInt32(int32_t& i);          bool ReadInt64(int64_t& i);          bool ReadShort(short& i);          bool ReadChar(char& c);          size_t ReadAll(char* szBuffer, size_t iLen) const;          bool IsEnd() const;          const char* GetCurrent() const{ return cur; }       public:          bool ReadLength(size_t & len);          bool ReadLengthWithoutOffset(size_t &headlen, size_t & outlen);      };       class BinaryWriteStream      {      public:          BinaryWriteStream(string* data);          virtual const char* GetData() const;          virtual size_t GetSize() const;          bool WriteCString(const char* str, size_t len);          bool WriteString(const string& str);          bool WriteDouble(double value, bool isNULL = false);          bool WriteInt64(int64_t value, bool isNULL = false);          bool WriteInt32(int32_t i, bool isNULL = false);          bool WriteShort(short i, bool isNULL = false);          bool WriteChar(char c, bool isNULL = false);          size_t GetCurrentPos() const{ return m_data->length(); }          void Flush();          void Clear();      private:          string* m_data;    };

其中BinaryWriteStream是編碼協議的類,BinaryReadStream是解碼協議的類。可以按下面這種方式來編碼和解碼。

編碼:

std::string outbuf;  BinaryWriteStream writeStream(&outbuf);  writeStream.WriteInt32(msg_type_register);  writeStream.WriteInt32(m_seq);  writeStream.WriteString(retData);  writeStream.Flush();

解碼:

BinaryReadStream readStream(strMsg.c_str(), strMsg.length());  int32_t cmd;  if (!readStream.ReadInt32(cmd))  {      return false;  }   //int seq;  if (!readStream.ReadInt32(m_seq))  {      return false;  }   std::string data;  size_t datalength;  if (!readStream.ReadString(&data, 0, datalength))  {      return false;  }

(七)、服務器程序結構的組織

由于內容過多,后續會單獨組織一篇文章詳細介紹

二、架構篇

一個項目的服務器端往往由很多服務組成,就算單個服務在性能上做到極致,支持的并發數量也是有限的,舉個簡單的例子,假如一個聊天服務器,每個用戶的信息是1k,那對于一個8G的內存的機器,在不考慮其它的情況下8*1024*1024*1024 / 100 = 1024,實際有838萬,但實際這只是非常理想的情況。所以我們有時候需要需要某個服務部署多套,就單個服務的實現來講還是《框架篇》中介紹的。我們舉個例子:

Linux高性能服務器架構設計方法是什么

這是蘑菇街TeamTalk的服務器架構。MsgServer是聊天服務,可以部署多套,每個聊天服務器啟動時都會告訴loginSever和routeSever自己的ip地址和端口號,當有用戶上下或者下線的時候,MsgServer也會告訴loginSever和routeSever自己上面最新的用戶數量和用戶id列表。現在一個用戶需要登錄,先連接loginServer,loginServer根據記錄的各個MsgServer上的用戶情況,返回一個最小負載的MsgServer的ip地址和端口號給客戶端,客戶端再利用這個ip地址和端口號去登錄MsgServer。當聊天時,位于A MsgServer上的用戶給另外一個用戶發送消息,如果該用戶不在同一個MsgServer上,MsgServer將消息轉發給RouteServer,RouteServer根據自己記錄的用戶id信息找到目標用戶所在的MsgServer并轉發給對應的MsgServer。

上面是分布式部署的一個例子。我們再來看另外一個例子,這個例子是單個服務的策略,實際服務器在處理網絡數據的時候,如果同時有多個socket上有數據要處理,可能會出現一直服務前幾個socket,直到前幾個socket處理完畢后再處理后面幾個socket的數據。這就相當于,你去飯店吃飯,大家都點了菜,但是有些桌子上一直在上菜,而有些桌子上一直沒有菜。這樣肯定不好,我們來看下如何避免這種現象:

int CFtdEngine::HandlePackage(CFTDCPackage *pFTDCPackage, CFTDCSession *pSession)  {      //NET_IO_LOG0("CFtdEngine::HandlePackage\n");      FTDC_PACKAGE_DEBUG(pFTDCPackage);       if (pFTDCPackage->GetTID() != FTD_TID_ReqUserLogin)      {          if (!IsSessionLogin(pSession->GetSessionID()))          {              SendErrorRsp(pFTDCPackage, pSession, 1, "客戶未登錄");              return 0;          }      }       CalcFlux(pSession, pFTDCPackage->Length());  //統計流量       REPORT_EVENT(LOG_DEBUG, "Front/Fgateway", "登錄請求%0x", pFTDCPackage->GetTID());       int nRet = 0;      switch(pFTDCPackage->GetTID())      {       case FTD_TID_ReqUserLogin:          ///huwp:20070608:檢查過高版本的API將被禁止登錄          if (pFTDCPackage->GetVersion()>FTD_VERSION)          {              SendErrorRsp(pFTDCPackage, pSession, 1, "Too High FTD Version");              return 0;          }          nRet = OnReqUserLogin(pFTDCPackage, (CFTDCSession *)pSession);          FTDRequestIndex.incValue();          break;      case FTD_TID_ReqCheckUserLogin:          nRet = OnReqCheckUserLogin(pFTDCPackage, (CFTDCSession *)pSession);          FTDRequestIndex.incValue();          break;      case FTD_TID_ReqSubscribeTopic:          nRet = OnReqSubscribeTopic(pFTDCPackage, (CFTDCSession *)pSession);          FTDRequestIndex.incValue();          break;        }       return 0;  }

當有某個socket上有數據可讀時,接著接收該socket上的數據,對接收到的數據進行解包,然后調用CalcFlux(pSession, pFTDCPackage->Length())進行流量統計:

void CFrontEngine::CalcFlux(CSession *pSession, const int nFlux)  {      TFrontSessionInfo *pSessionInfo = m_mapSessionInfo.Find(pSession->GetSessionID());      if (pSessionInfo != NULL)      {          //流量控制改為計數          pSessionInfo->nCommFlux ++;          ///若流量超過規定,則掛起該會話的讀操作          if (pSessionInfo->nCommFlux >= pSessionInfo->nMaxCommFlux)          {              pSession->SuspendRead(true);          }      }  }

該函數會先讓某個連接會話(Session)處理的包數量遞增,接著判斷是否超過最大包數量,則設置讀掛起標志:

void CSession::SuspendRead(bool bSuspend)  {      m_bSuspendRead = bSuspend;  }

這樣下次將會從檢測的socket列表中排除該socket:

void CEpollReactor::RegisterIO(CEventHandler *pEventHandler)  {      int nReadID, nWriteID;      pEventHandler->GetIds(&nReadID, &nWriteID);      if (nWriteID != 0 && nReadID ==0)      {          nReadID = nWriteID;      }      if (nReadID != 0)      {          m_mapEventHandlerId[pEventHandler] = nReadID;          struct epoll_event ev;          ev.data.ptr = pEventHandler;          if(epoll_ctl(m_fdEpoll, EPOLL_CTL_ADD, nReadID, &ev) != 0)          {              perror("epoll_ctl EPOLL_CTL_ADD");          }      }  }   void CSession::GetIds(int *pReadId, int *pWriteId)  {      m_pChannelProtocol->GetIds(pReadId,pWriteId);      if (m_bSuspendRead)      {          *pReadId = 0;      }  }

也就是說不再檢測該socket上是否有數據可讀。然后在定時器里1秒后重置該標志,這樣這個socket上有數據的話又可以重新檢測到了:

const int SESSION_CHECK_TIMER_ID    = 9;  const int SESSION_CHECK_INTERVAL    = 1000;   SetTimer(SESSION_CHECK_TIMER_ID, SESSION_CHECK_INTERVAL);   void CFrontEngine::OnTimer(int nIDEvent)  {      if (nIDEvent == SESSION_CHECK_TIMER_ID)      {          CSessionMap::iterator itor = m_mapSession.Begin();          while (!itor.IsEnd())          {              TFrontSessionInfo *pFind = m_mapSessionInfo.Find((*itor)->GetSessionID());              if (pFind != NULL)              {                  CheckSession(*itor, pFind);              }              itor++;          }      }  }   void CFrontEngine::CheckSession(CSession *pSession, TFrontSessionInfo *pSessionInfo)  {      ///重新開始計算流量      pSessionInfo->nCommFlux -= pSessionInfo->nMaxCommFlux;      if (pSessionInfo->nCommFlux < 0)      {          pSessionInfo->nCommFlux = 0;      }      ///若流量超過規定,則掛起該會話的讀操作      pSession->SuspendRead(pSessionInfo->nCommFlux >= pSessionInfo->nMaxCommFlux); }

這就相當與飯店里面先給某一桌客人上一些菜,讓他們先吃著,等上了一些菜之后不會再給這桌繼續上菜了,而是給其它空桌上菜,大家都吃上后,繼續回來給原先的桌子繼續上菜。實際上我們的飯店都是這么做的。上面的例子是單服務流量控制的實現的一個非常好的思路,它保證了每個客戶端都能均衡地得到服務,而不是一些客戶端等很久才有響應。

另外加快服務器處理速度的策略可能就是緩存了,緩存實際上是以空間換取時間的策略。對于一些反復使用的,但是不經常改變的信息,如果從原始地點加載這些信息就比較耗時的數據(比如從磁盤中、從數據庫中),我們就可以使用緩存。所以時下像redis、leveldb、fastdb等各種內存數據庫大行其道。我在flamingo中用戶的基本信息都是緩存在聊天服務程序中的,而文件服務啟動時會去加載指定目錄里面的所有程序名稱,這些文件的名稱都是md5,為該文件內容的md5。這樣當客戶端上傳了新文件請求時,如果其傳上來的文件md5已經位于緩存中,則表明該文件在服務器上已經存在,這個時候服務器就不必再接收該文件了,而是告訴客戶端文件已經上傳成功了。

到此,相信大家對“Linux高性能服務器架構設計方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

新郑市| 海口市| 红安县| 慈溪市| 东海县| 阿勒泰市| 海原县| 故城县| 若羌县| 土默特左旗| 柞水县| 台山市| 视频| 岐山县| 福建省| 中方县| 桑日县| 徐水县| 浦江县| 凯里市| 南郑县| 饶阳县| 白城市| 洪江市| 荥经县| 剑川县| 凤山县| 宁强县| 枣阳市| 旌德县| 平顶山市| 赣州市| 呼伦贝尔市| 义乌市| 行唐县| 温宿县| 恩施市| 靖宇县| 云龙县| 原平市| 梁平县|