物聯網高併發編程之P2P技術NAT穿越方案

物聯網高併發編程之P2P技術NAT穿越方案

更多物聯網高併發編程知識請移步:https://www.yuque.com/shizhiy...編程

內容概述

P2P即點對點通訊,或稱爲對等聯網,與傳統的服務器客戶端模式(以下圖「P2P結構模型」所示)有着明顯的區別,在即時通信方案中應用普遍(好比IM應用中的實時音視頻通訊、實時文件傳輸甚至文字聊天等)。安全

P2P能夠是一種通訊模式、一種邏輯網絡模型、一種技術、甚至一種理念。服務器

在P2P網絡中(如右圖所示),全部通訊節點的地位都是對等的,每一個節點都扮演着客戶機和服務器雙重角色,節點之間經過直接通訊實現文件信息、處理器運算能力、存儲空間等資源的共享。網絡

P2P網絡具備分散性、可擴展性、健壯性等特色,這使得P2P技術在信息共享、即時通信、協同工做、分佈式計算、網絡存儲等領域都有廣闊的應用。



1經典的CS模式:                                                        
 session

 P2P結構模型:
併發

NAT技術和P2P技術做爲經典的兩項網絡技術,在如今的網絡上有着普遍的應用,P2P主機位於NAT網關後面的狀況家常便飯。NAT技術雖然在必定程度上解決了IPv4地址短缺的問題,在構建防火牆、保證網絡安全方面都發揮了必定的做用,卻破壞了端到端的網絡通訊。NAT阻礙主機進行P2P通訊的主要緣由是NAT不容許外網主機主動訪問內網主機,可是P2P技術卻要求通訊雙方都能主動發起訪問,因此要在NAT網絡環境中進行有效的P2P通訊,就必須採用新的解決方案。異步

P2P做爲一項實用的技術,有很大的優化空間,而且相對於網絡設備,基於P2P的應用程序在實現上更爲靈活。因此爲了兼容NAT,基於P2P的應用程序在開發的時候大多會根據自身特色加入一些穿越NAT的功能以解決上述問題。如下着重介紹幾種常見的P2P穿越NAT方案。
**
分佈式

反向連接技術

一種特殊的P2P場景(通訊雙方中只有一方位於NAT設備以後)

此種狀況是全部P2P場景中最簡單的,它使用一種被稱爲「反向連接技術」來解決這個問題。大體的原理以下所述。函數

如圖所示,客戶端A位於NAT以後,它經過TCP端口1234鏈接到服務器的TCP端口1235上,NAT設備爲這個鏈接從新分配了TCP端口62000。客戶端B也經過TCP端口1234鏈接到服務器端口1235上。A和B從服務器處獲知的對方的外網地址二元組{IP地址:端口號}分別爲{138.76.29.7:1234}和{155.99.25.11:62000},它們在各自的本地端口上進行偵聽。高併發

因爲B 擁有外網IP地址,因此A要發起與B的通訊,能夠直接經過TCP鏈接到B。
但若是B嘗試經過TCP鏈接到A進行P2P通訊,則會失敗,緣由是A位於NAT設備後,雖然B發出的TCP SYN請求可以到達NAT設備的端口62000,但NAT設備會拒絕這個鏈接請求。

要想與Client A通訊, B不是直接向A發起鏈接,而是經過服務器給A轉發一個鏈接請求,反過來請求A鏈接到B(即進行反向連接),A在收到從服務器轉發過來的請求之後,會主動向B發起一個TCP的鏈接請求,這樣在NAT設備上就會創建起關於這個鏈接的相關表項,使A和B之間可以正常通訊,從而創建起它們之間的TCP鏈接。

image.png

基於UDP協議的P2P打洞技術

原理概述

UDP打洞技術是經過中間服務器的協助在各自的NAT網關上創建相關的表項,使P2P鏈接的雙方發送的報文可以直接穿透對方的NAT網關,從而實現P2P客戶端互連。若是兩臺位於NAT設備後面的P2P客戶端但願在本身的NAT網關上打個洞,那麼他們須要一個協助者——集中服務器,而且還須要一種用於打洞的Session創建機制。

什麼是集中服務器?

集中服務器本質上是一臺被設置在公網上的服務器,創建P2P的雙方均可以直接訪問到這臺服務器。位於NAT網關後面的客戶端A和B均可以與一臺已知的集中服務器創建鏈接,並經過這臺集中服務器瞭解對方的信息並中轉各自的信息。
同時集中服務器的另外一個重要做用在於判斷某個客戶端是否在NAT網關以後。

具體的方法是:一個客戶端在集中服務器上登錄的時候,服務器記錄下該客戶端的兩對地址二元組信息{IP地址:UDP端口},一對是該客戶端與集中服務器進行通訊的自身的IP地址和端口號,另外一對是集中服務器記錄下的由服務器「觀察」到的該客戶端實際與本身通訊所使用的IP地址和端口號。咱們能夠把前一對地址二元組看做是客戶端的內網IP地址和端口號,把後一對地址二元組看做是客戶端的內網IP地址和端口號通過NAT轉換後的外網IP地址和端口號。集中服務器能夠從客戶端的登錄消息中獲得該客戶端的內網相關信息,還能夠經過登錄消息的IP頭和UDP頭獲得該客戶端的外網相關信息。若是該客戶端不是位於NAT設備後面,那麼採用上述方法獲得的兩對地址二元組信息是徹底相同的。



P2P的Session創建原理:
假定客戶端A要發起對客戶端B的直接鏈接,具體的「打洞」過程以下:

  • 1)A最初不知道如何向客戶端B發起鏈接,因而A向集中服務器發送消息,請求集中服務器幫助創建與客戶端B的UDP鏈接。
  • 2)集中服務器將含有B的外網和內網的地址二元組發給A,同時,集中服務器將包含有A的外網和內網的地址二元組信息的消息也發給B。這樣一來, A與B就都知道對方外網和內網的地址二元組信息了。
  • 3)當A收到由集中服務器發來的包含B的外網和內網的地址二元組信息後, A開始向B的地址二元組發送UDP數據包,而且A會自動鎖定第一個給出響應的B的地址二元組。同理,當B收到由集中服務器發來的A的外網和內網地址二元組信息後,也會開始向A的外網和內網的地址二元組發送UDP數據包,而且自動鎖定第一個獲得A迴應的地址二元組。因爲A與B互相向對方發送UDP數據包的操做是異步的,因此A和B發送數據包的時間前後並無時序要求。

下面來看下這三者之間是如何進行UDP打洞的。在這咱們分三種具體情景來討論:

  • 第一種是最簡單的一種情景,兩個客戶端都位於同一個NAT設備後面,即位於同一內網中;
  • 第二種是最廣泛的一種情景,兩個客戶端分別位於不一樣的NAT設備後面,分屬不一樣的內網;
  • 第三種是客戶端位於兩層NAT設備以後,一般最上層的NAT是由網絡提供商提供的,第二層NAT是家用的NAT路由器之類的設備提供的。

典型P2P情景1:  兩客戶端位於同一NAT設備後面

這是最簡單的一種狀況(如圖4所示):客戶端A和B分別與集中服務器創建UDP鏈接,通過NAT轉換後,A的公網端口被映射爲62000,B的公網端口映射爲62005。



位於同一個NAT設備後的UDP打洞過程:
 

當A向集中服務器發出消息請求與B進行鏈接,集中服務器將B的外網地址二元組以及內網地址二元組發給A,同時把A的外網以及內網的地址二元組信息發給B。A和B發往對方公網地址二元組信息的UDP數據包不必定會被對方收到,這取決於當前的NAT設備是否支持不一樣端口之間的UDP數據包可否到達(即Hairpin轉換特性),不管如何A與B發往對方內網的地址二元組信息的UDP數據包是必定能夠到達的,內網數據包不須要路由,且速度更快。A與B推薦採用內網的地址二元組信息進行常規的P2P通訊。

假定NAT設備支持Hairpin轉換,P2P雙方也應忽略與內網地址二元組的鏈接,若是A 和B採用外網的地址二元組作爲P2P通訊的鏈接,這勢必會形成數據包無謂地通過NAT設備,這是一種對資源的浪費。就目前的網絡狀況而言,應用程序在「打洞」的時候,最好仍是把外網和內網的地址二元組都嘗試一下。若是都能成功,優先之內網地址進行鏈接。

什麼是Hairpin技術?

Hairpin技術又被稱爲Hairpin NAT、Loopback NAT或Hairpin Translation。Hairpin技術須要NAT網關支持,它可以讓兩臺位於同一臺NAT網關後面的主機,經過對方的公網地址和端口相互訪問,NAT網關會根據一系列規則,將對內部主機發往其NAT公網IP地址的報文進行轉換,並從私網接口發送給目標主機。目前有不少NAT設備不支持該技術,這種狀況下,NAT網關在一些特定場合下將會阻斷P2P穿越NAT的行爲,打洞的嘗試是沒法成功的。好在如今已經有愈來愈多的NAT設備商開始加入到對該轉換的支持中來。

典型P2P情景2: 兩客戶端位於不一樣的NAT設備後面

這是最廣泛的一種狀況(如圖5所示):客戶端A與B經由各自的NAT設備與集中服務器創建UDP鏈接, A與B的本地端口號均爲4321,集中服務器的公網端口號爲1234。在向外的會話中, A的外網IP被映射爲155.99.25.11,外網端口爲62000;B的外網IP被映射爲138.76.29.7,外網端口爲31000。



以下所示:
**

客戶端A——>本地IP:10.0.0.1,本地端口:4321,外網IP:155.99.25.11,外網端口:62000
客戶端B——>本地IP:10.1.1.3,本地端口:4321,外網IP:138.76.29.7,外網端口:31000

位於不一樣NAT設備後的UDP打洞過程:

在A向服務器發送的登錄消息中,包含有A的內網地址二元組信息,即10.0.0.1:4321;服務器會記錄下A的內網地址二元組信息,同時會把本身觀察到的A的外網地址二元組信息記錄下來。同理,服務器也會記錄下B的內網地址二元組信息和由服務器觀察到的客戶端B的外網地址二元組信息。不管A與B兩者中的任何一方向服務器發送P2P鏈接請求,服務器都會將其記錄下來的上述的外網和內網地址二元組發送給A或B。

A和B分屬不一樣的內網,它們的內網地址在外網中是沒有路由的,因此發往各自內網地址的UDP數據包會發送到錯誤的主機或者根本不存在的主機上。當A的第一個消息發往B的外網地址(如圖3所示),該消息途經A的NAT設備,並在該設備上生成一個會話表項,該會話的源地址二元組信息是{10.0.0.1:4321},和A與服務器創建鏈接的時候NAT生成的源地址二元組信息同樣,但它的目的地址是B的外網地址。在A的NAT設備支持保留A的內網地址二元組信息的狀況下,全部來自A的源地址二元組信息爲{10.0.0.1:4321}的數據包都沿用A與集中服務器事先創建起來的會話,這些數據包的外網地址二元組信息均被映射爲{155.99.25.11:62000}。

A向B的外網地址發送消息的過程就是「打洞」的過程,從A的內網的角度來看應爲從{10.0.0.1:4321}發往{138.76.29.7:31000},從A在其NAT設備上創建的會話來看,是從{155.99.25.11:62000}發到{138.76.29.7:31000}。若是A發給B的外網地址二元組的消息包在B向A發送消息包以前到達B的NAT設備,B的NAT設備會認爲A發過來的消息是未經受權的外網消息,並丟棄該數據包。

B發往A的消息包也會在B的NAT設備上創建一個{10.1.1.3:4321,155.99.25.11:62000}的會話(一般也會沿用B與集中服務器鏈接時創建的會話,只是該會話如今不只接受由服務器發給B的消息,還能夠接受從A的NAT設備{155.99.25.11:6200}發來的消息)。

一旦A與B都向對方的NAT設備在外網上的地址二元組發送了數據包,就打開了A與B之間的「洞」,A與B向對方的外網地址發送數據,等效爲向對方的客戶端直接發送UDP數據包了。一旦應用程序確認已經能夠經過往對方的外網地址發送數據包的方式讓數據包到達NAT後面的目的應用程序,程序會自動中止繼續發送用於「打洞」的數據包,轉而開始真正的P2P數據傳輸。 

典型P2P情景3: 兩客戶端位於兩層(或多層)NAT設備以後

此種情景最典型的部署狀況就像這樣:最上層的NAT設備一般是由網絡提供商(ISP)提供,下層NAT設備是家用路由器。

如圖所示:假定NAT C是由ISP提供的NAT設備,NAT C提供將多個用戶節點映射到有限的幾個公網IP的服務,NAT A和NAT B做爲NAT C的內網節點將把用戶的內部網絡接入NAT C的內網,用戶的內部網絡就能夠經由NAT C訪問公網了。從這種拓撲結構上來看,只有服務器與NAT C是真正擁有公網可路由IP地址的設備,而NAT A和NAT B所使用的公網IP地址,其實是由ISP服務提供商設定的(相對於NAT C而言)內網地址(咱們將這種由ISP提供的內網地址稱之爲「僞」公網地址)。同理,隸屬於NAT A與NAT B的客戶端,它們處於NAT A,NAT B的內網,以此類推,客戶端能夠放到到多層NAT設備後面。客戶端A和客戶端B發起對服務器S的鏈接的時候,就會依次在NAT A和NAT B上創建向外的Session,而NAT A、NAT B要聯入公網的時候,會在NAT C上再創建向外的Session。



image.png



如今假定客戶端A和B但願經過UDP「打洞」完成兩個客戶端的P2P直連。最優化的路由策略是客戶端A向客戶端B的「僞公網」IP上發送數據包,即ISP服務提供商指定的內網IP,NAT B的「僞」公網地址二元組,{10.0.1.2:55000}。因爲從服務器的角度只能觀察到真正的公網地址,也就是NAT A,NAT B在NAT C創建session的真正的公網地址{155.99.25.11:62000}以及{155.99.25.11:62005},很是不幸的是客戶端A與客戶端B是沒法經過服務器知道這些「僞」公網的地址,並且即便客戶端A和B經過某種手段能夠獲得NAT A和NAT B的「僞」公網地址,咱們仍然不建議採用上述的「最優化」的打洞方式,這是由於這些地址是由ISP服務提供商提供的或許會存在與客戶端自己所在的內網地址重複的可能性(例如:NAT A的內網的IP地址域剛好與NAT A在NAT C的「僞」公網IP地址域重複,這樣就會致使打洞數據包沒法發出的問題)。

所以客戶端別無選擇,只能使用由公網服務器觀察到的A,B的公網地址二元組進行「打洞」操做,用於「打洞」的數據包將由NAT C進行轉發。

當客戶端A向客戶端B的公網地址二元組{155.99.25.11:62005}發送UDP數據包的時候,NAT A首先把數據包的源地址二元組由A的內網地址二元組{10.0.0.1:4321}轉換爲「僞」公網地址二元組{10.0.1.1:45000},如今數據包到了NAT C,NAT C應該能夠識別出來該數據包是要發往自身轉換過的公網地址二元組,若是NAT C能夠給出「合理」響應的話,NAT C將把該數據包的源地址二元組改成{155.99.25.11:62000},目的地址二元組改成{10.0.1.2:55000},即NAT B的「僞」公網地址二元組,NAT B最後會將收到的數據包發往客戶端B。一樣,由B發往A的數據包也會通過相似的過程。目前也有不少NAT設備不支持相似這樣的「Hairpin轉換」,可是已經有愈來愈多的NAT設備商開始加入對該轉換的支持中來。


一個須要考慮的現實問題:UDP在空閒狀態下的超時

固然,從應用的角度上來講,在完成打洞過程的同時,還有一些技術問題須要解決,如UDP在空閒狀態下的超時問題。因爲UDP轉換協議提供的「洞」不是絕對可靠的,多數NAT設備內部都有一個UDP轉換的空閒狀態計時器,若是在一段時間內沒有UDP數據通訊,NAT設備會關掉由「打洞」過程打出來的「洞」。若是P2P應用程序但願「洞」的存活時間不受NAT網關的限制,就最好在穿越NAT之後設定一個穿越的有效期。

對於有效期目前沒有標準值,它與NAT設備內部的配置有關,某些設備上最短的只有20秒左右。在這個有效期內,即便沒有P2P數據包須要傳輸,應用程序爲了維持該「洞」能夠正常工做,也必須向對方發送「打洞」心跳包。這個心跳包是須要雙方應用程序都發送的,只有一方發送不會維持另外一方的Session正常工做。除了頻繁發送「打洞」心跳包之外,還有一個方法就是在當前的「洞」超時以前,P2P客戶端雙方從新「打洞」,丟棄原有的「洞」,這也不失爲一個有效的方法。


基於TCP協議的P2P打洞技術詳細

創建穿越NAT設備的P2P的TCP鏈接只比UDP複雜一點點,TCP協議的」「打洞」從協議層來看是與UDP的「打洞」過程很是類似的。儘管如此,基於TCP協議的打洞至今爲止尚未被很好的理解,這也形成了的對其提供支持的NAT設備不是不少。在NAT設備支持的前提下,基於TCP的「打洞」技術實際上與基於UDP的「打洞」技術同樣快捷、可靠。實際上,只要NAT設備支持的話,基於TCP的P2P技術的健壯性將比基於UDP技術的更強一些,由於TCP協議的狀態機給出了一種標準的方法來精確的獲取某個TCP session的生命期,而UDP協議則沒法作到這一點。
**

套接字和TCP端口的重用

實現基於TCP協議的P2P打洞過程當中,最主要的問題不是來自於TCP協議,而是來自於應用程序的API接口。這是因爲標準的伯克利(Berkeley)套接字的API是圍繞着構建客戶端/服務器程序而設計的,API容許TCP流套接字經過調用connect()函數來創建向外的鏈接,或者經過listen()和accept函數接受來自外部的鏈接,可是,API不提供相似UDP那樣的,同一個端口既能夠向外鏈接,又可以接受來自外部的鏈接。並且更糟的是,TCP的套接字一般僅容許創建1對1的響應,即應用程序在將一個套接字綁定到本地的一個端口之後,任何試圖將第二個套接字綁定到該端口的操做都會失敗。

爲了讓TCP「打洞」可以順利工做,咱們須要使用一個本地的TCP端口來監聽來自外部的TCP鏈接,同時創建多個向外的TCP鏈接。幸運的是,全部的主流操做系統都可以支持特殊的TCP套接字參數,一般叫作「SO_REUSEADDR」,該參數容許應用程序將多個套接字綁定到本地的一個地址二元組(只要全部要綁定的套接字都設置了SO_REUSEADDR參數便可)。BSD系統引入了SO_REUSEPORT參數,該參數用於區分端口重用仍是地址重用,在這樣的系統裏面,上述全部的參數必須都設置才行。


打開P2P的TCP流

假定客戶端A但願創建與B的TCP鏈接。咱們像一般同樣假定A和B已經與公網上的已知服務器創建了TCP鏈接。服務器記錄下來每一個接入的客戶端的公網和內網的地址二元組,如同爲UDP服務的時候同樣。



從協議層來看,TCP「打洞」與UDP「打洞」是幾乎徹底相同的過程:

  • 客戶端A使用其與服務器的鏈接向服務器發送請求,要求服務器協助其鏈接客戶端B;
  • 服務器將B的公網和內網的TCP地址的二元組信息返回給A,同時,服務器將A的公網和內網的地址二元組也發送給B;
  • 客戶端A和B使用鏈接服務器的端口異步地發起向對方的公網、內網地址二元組的TCP鏈接,同時監聽各自的本地TCP端口是否有外部的鏈接聯入;
  • A和B開始等待向外的鏈接是否成功,檢查是否有新鏈接聯入。若是向外的鏈接因爲某種網絡錯誤而失敗,如:「鏈接被重置」或者「節點沒法訪問」,客戶端只須要延遲一小段時間(例如延遲一秒鐘),而後從新發起鏈接便可,延遲的時間和重複鏈接的次數能夠由應用程序編寫者來肯定;
  • TCP鏈接創建起來之後,客戶端之間應該開始鑑權操做,確保目前聯入的鏈接就是所但願的鏈接。若是鑑權失敗,客戶端將關閉鏈接,而且繼續等待新的鏈接聯入。客戶端一般採用「先入爲主」的策略,只接受第一個經過鑑權操做的客戶端,而後將進入P2P通訊過程再也不繼續等待是否有新的鏈接聯入。

TCP打洞:

與UDP不一樣的是,由於使用UDP協議的每一個客戶端只須要一個套接字便可完成與服務器的通訊,而TCP客戶端必須處理多個套接字綁定到同一個本地TCP端口的問題,如圖7所示。如今來看實際中常見的一種情景,A與B分別位於不一樣的NAT設備後面,如圖5所示,而且假定圖中的端口號是TCP協議的端口號,而不是UDP的端口號。圖中向外的鏈接表明A和B向對方的內網地址二元組發起的鏈接,這些鏈接或許會失敗或者沒法鏈接到對方。如同使用UDP協議進行「打洞」操做遇到的問題同樣,TCP的「打洞」操做也會遇到內網的IP與「僞」公網IP重複形成鏈接失敗或者錯誤鏈接之類的問題。

客戶端向彼此公網地址二元組發起鏈接的操做,會使得各自的NAT設備打開新的「洞」容許A與B的TCP數據經過。若是NAT設備支持TCP「打洞」操做的話,一個在客戶端之間的基於TCP協議的流通道就會自動創建起來。若是A向B發送的第一個SYN包發到了B的NAT設備,而B在此前沒有向A發送SYN包,B的NAT設備會丟棄這個包,這會引發A的「鏈接失敗」或「沒法鏈接」問題。而此時,因爲A已經向B發送過SYN包,B發往A的SYN包將被看做是由A發往B的包的迴應的一部分,因此B發往A的SYN包會順利地經過A的NAT設備,到達A,從而創建起A與B的P2P鏈接。


從應用程序的角度來看TCP「打洞」

從應用程序的角度來看,在進行TCP「打洞」的時候都發生了什麼呢?假定A首先向B發出SYN包,該包發往B的公網地址二元組,而且被B的NAT設備丟棄,可是B發往A的公網地址二元組的SYN包則經過A的NAT到達了A,而後,會發生如下的兩種結果中的一種,具體是哪種取決於操做系統對TCP協議的實現:

(1)A的TCP實現會發現收到的SYN包就是其發起鏈接並但願聯入的B的SYN包,通俗一點來講就是「說曹操,曹操到」的意思,原本A要去找B,結果B本身找上門來了。A的TCP協議棧所以會把B做爲A向B發起鏈接connect的一部分,並認爲鏈接已經成功。程序A調用的異步connect()函數將成功返回,A的listen()等待從外部聯入的函數將沒有任何反映。此時,B聯入A的操做在A程序的內部被理解爲A聯入B鏈接成功,而且A開始使用這個鏈接與B開始P2P通訊。

因爲收到的SYN包中不包含A須要的ACK數據,所以,A的TCP將用SYN-ACK包迴應B的公網地址二元組,而且將使用先前A發向B的SYN包同樣的序列號。一旦B的TCP收到由A發來的SYN-ACK包,則把本身的ACK包發給A,而後兩端創建起TCP鏈接。簡單的說,第一種,就是即便A發往B的SYN包被B的NAT丟棄了,可是因爲B發往A的包到達了A。結果是,A認爲本身鏈接成功了,B也認爲本身鏈接成功了,無論是誰成功了,總之鏈接是已經創建起來了。

(2)另一種結果是,A的TCP實現沒有像(1)中所講的那麼「智能」,它沒有發現如今聯入的B就是本身但願聯入的。就比如在機場接人,明明遇到了本身想要接的人卻不認識,誤認爲是其餘的人,安排別人給接走了,後來才知道是本身錯過了機會,可是不管如何,人已經接到了任務已經完成了。而後,A經過常規的listen()函數和accept()函數獲得與B的鏈接,而由A發起的向B的公網地址二元組的鏈接會以失敗了結。儘管A向B的鏈接失敗,A仍然獲得了B發起的向A的鏈接,等效於A與B之間已經聯通,無論中間過程如何,A與B已經鏈接起來了,結果是A和B的基於TCP協議的P2P鏈接已經創建起來了。

第一種結果適用於基於BSD的操做系統對於TCP的實現,而第二種結果更加廣泛一些,多數Linux和Windows系統都會按照第二種結果來處理。

總結

在IP地址極度短缺的今天,NAT幾乎已是無所不在的一項技術了,以致於如今任何一項新技術都不得不考慮和NAT的兼容。做爲當下應用最普遍的技術之一,P2P技術也必然要面對NAT這個障礙。

打洞技術看起來是一項近彷佛蠻幹的技術,卻不失爲一種有效的技術手段。在集中服務器的幫助下,P2P的雙方利用端口預測的技術在NAT網關上打出通道,從而實現NAT穿越,解決了NAT對於P2P的阻隔,爲P2P技術在網絡中更普遍的推廣做出了很是大的貢獻。

相關文章
相關標籤/搜索