溫馨提示×

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

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么用取消參數(shù)使Go net/http服務(wù)更靈活

發(fā)布時(shí)間:2021-10-25 17:02:28 來(lái)源:億速云 閱讀:133 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“怎么用取消參數(shù)使Go net/http服務(wù)更靈活”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“怎么用取消參數(shù)使Go net/http服務(wù)更靈活”吧!

服務(wù)超時(shí) — 基本原理

web  編程中,超時(shí)通常分為客戶端和服務(wù)端超時(shí)兩種。我之所以要研究這個(gè)主題,是因?yàn)槲易约河龅搅艘粋€(gè)有意思的服務(wù)端超時(shí)的問(wèn)題。這也是本文我們將要重點(diǎn)討論服務(wù)側(cè)超時(shí)的原因。

先解釋下基本術(shù)語(yǔ):超時(shí)是一個(gè)時(shí)間間隔(或邊界),用來(lái)標(biāo)識(shí)在這個(gè)時(shí)間段內(nèi)要完成特定的行為。如果在給定的時(shí)間范圍內(nèi)沒(méi)有完成操作,就產(chǎn)生了超時(shí),這個(gè)操作會(huì)被取消。

從一個(gè) net/http 的服務(wù)的初始化中,能看出一些超時(shí)的基礎(chǔ)配置:

srv := &http.Server{     ReadTimeout:       1 * time.Second,     WriteTimeout:      1 * time.Second,     IdleTimeout:       30 * time.Second,     ReadHeaderTimeout: 2 * time.Second,     TLSConfig:         tlsConfig,     Handler:           srvMux, }

http.Server 類型的服務(wù)可以用四個(gè)不同的 timeout 來(lái)初始化:

  • ReadTimeout:讀取包括請(qǐng)求體的整個(gè)請(qǐng)求的最大時(shí)長(zhǎng)

  • WriteTimeout:寫響應(yīng)允許的最大時(shí)長(zhǎng)

  • IdleTimetout:當(dāng)開啟了保持活動(dòng)狀態(tài)(keep-alive)時(shí)允許的最大空閑時(shí)間

  • ReadHeaderTimeout:允許讀請(qǐng)求頭的最大時(shí)長(zhǎng)

對(duì)上述超時(shí)的圖表展示:

怎么用取消參數(shù)使Go net/http服務(wù)更靈活

服務(wù)生命周期和超時(shí)

當(dāng)心!不要以為這些就是你所需要的所有的超時(shí)了。除此之外還有很多超時(shí),這些超時(shí)提供了更小的粒度控制,對(duì)于我們的持續(xù)運(yùn)行的 HTTP 處理器不會(huì)生效。

請(qǐng)聽我解釋。

timeout 和 deadline

如果我們查看 net/http 的源碼,尤其是看到 `conn` 類型[1] 時(shí),我們會(huì)發(fā)現(xiàn)conn 實(shí)際上使用了 net.Conn  連接,net.Conn 表示底層的網(wǎng)絡(luò)連接:

// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L247 // A conn represents the server-side of an HTTP connection. type conn struct {     // server is the server on which the connection arrived.     // Immutable; never nil.     server *Server      // * Snipped *      // rwc is the underlying network connection.     // This is never wrapped by other types and is the value given out     // to CloseNotifier callers. It is usually of type *net.TCPConn or     // *tls.Conn.     rwc net.Conn      // * Snipped * }

換句話說(shuō),我們的 HTTP 請(qǐng)求實(shí)際上是基于 TCP 連接的。從類型上看,TLS 連接是 *net.TCPConn 或 *tls.Conn 。

serve 函數(shù)[2]處理每一個(gè)請(qǐng)求[3]時(shí)調(diào)用 readRequest 函數(shù)。readRequest使用我們?cè)O(shè)置的 timeout 值[4]來(lái)設(shè)置  TCP 連接的 deadline:

// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L936 // Read next request from connection. func (c *conn) readRequest(ctx context.Context) (w *response, err error) {         // *Snipped*          t0 := time.Now()         if d := c.server.readHeaderTimeout(); d != 0 {                 hdrDeadline = t0.Add(d)         }         if d := c.server.ReadTimeout; d != 0 {                 wholeReqDeadline = t0.Add(d)         }         c.rwc.SetReadDeadline(hdrDeadline)         if d := c.server.WriteTimeout; d != 0 {                 defer func() {                         c.rwc.SetWriteDeadline(time.Now().Add(d))                 }()         }          // *Snipped* }

從上面的摘要中,我們可以知道:我們對(duì)服務(wù)設(shè)置的 timeout 值最終表現(xiàn)為 TCP 連接的 deadline 而不是 HTTP 超時(shí)。

所以,deadline 是什么?工作機(jī)制是什么?如果我們的請(qǐng)求耗時(shí)過(guò)長(zhǎng),它們會(huì)取消我們的連接嗎?

一種簡(jiǎn)單地理解 deadline 的思路是,把它理解為對(duì)作用于連接上的特定的行為的發(fā)生限制的一個(gè)時(shí)間點(diǎn)。例如,如果我們?cè)O(shè)置了一個(gè)寫的  deadline,當(dāng)過(guò)了這個(gè) deadline 后,所有對(duì)這個(gè)連接的寫操作都會(huì)被拒絕。

盡管我們可以使用 deadline 來(lái)模擬超時(shí)操作,但我們還是不能控制處理器完成操作所需的耗時(shí)。deadline  作用于連接,因此我們的服務(wù)僅在處理器嘗試訪問(wèn)連接的屬性(如對(duì) http.ResponseWriter 進(jìn)行寫操作)之后才會(huì)返回(錯(cuò)誤)結(jié)果。

為了實(shí)際驗(yàn)證上面的論述,我們來(lái)創(chuàng)建一個(gè)小的 handler,這個(gè) handler 完成操作所需的耗時(shí)相對(duì)于我們?yōu)榉?wù)設(shè)置的超時(shí)更長(zhǎng):

package main  import (  "fmt"  "io"  "net/http"  "time" )  func slowHandler(w http.ResponseWriter, req *http.Request) {  time.Sleep(2 * time.Second)  io.WriteString(w, "I am slow!\n") }  func main() {  srv := http.Server{   Addr:         ":8888",   WriteTimeout: 1 * time.Second,   Handler:      http.HandlerFunc(slowHandler),  }   if err := srv.ListenAndServe(); err != nil {   fmt.Printf("Server failed: %s\n", err)  } }

上面的服務(wù)有一個(gè) handler,這個(gè) handler 完成操作需要兩秒。另一方面,http.Server 的 WriteTimeout 屬性設(shè)為 1  秒。基于服務(wù)的這些配置,我們猜測(cè) handler 不能把響應(yīng)寫到連接。

我們可以用 go run server.go 來(lái)啟動(dòng)服務(wù)。使用 curl localhost:8888 來(lái)發(fā)送一個(gè)請(qǐng)求:

$ time curl localhost:8888 curl: (52) Empty reply from server curl localhost:8888  0.01s user 0.01s system 0% CPU 2.021 total

這個(gè)請(qǐng)求需要兩秒來(lái)完成處理,服務(wù)返回的響應(yīng)是空的。雖然我們的服務(wù)知道在 1 秒之后我們寫不了響應(yīng)了,但 handler 還是多耗了 100% 的時(shí)間(2  秒)來(lái)完成處理。

雖然這是個(gè)類似超時(shí)的處理,但它更大的作用是在到達(dá)超時(shí)時(shí)間時(shí),阻止服務(wù)進(jìn)行更多的操作,結(jié)束請(qǐng)求。在我們上面的例子中,handler  在完成之前一直在處理請(qǐng)求,即使已經(jīng)超出響應(yīng)寫超時(shí)時(shí)間(1 秒)100%(耗時(shí) 2 秒)。

最根本的問(wèn)題是,對(duì)于處理器來(lái)說(shuō),我們應(yīng)該怎么設(shè)置超時(shí)時(shí)間才更有效?

處理超時(shí)

我們的目標(biāo)是確保我們的 slowHandler 在 1s 內(nèi)完成處理。如果超過(guò)了 1s,我們的服務(wù)會(huì)停止運(yùn)行并返回對(duì)應(yīng)的超時(shí)錯(cuò)誤。

在 Go 和一些其它編程語(yǔ)言中,組合往往是設(shè)計(jì)和開發(fā)中最好的方式。標(biāo)準(zhǔn)庫(kù)的 `net/http`  包[5]有很多相互兼容的元素,開發(fā)者可以不需經(jīng)過(guò)復(fù)雜的設(shè)計(jì)考慮就可以輕易將它們組合在一起。

基于此,net/http 包提供了`TimeoutHandler`[6] — 返回了一個(gè)在給定的時(shí)間限制內(nèi)運(yùn)行的 handler。

函數(shù)簽名:

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

第一個(gè)參數(shù)是 Handler,第二個(gè)參數(shù)是 time.Duration (超時(shí)時(shí)間),第三個(gè)參數(shù)是 string  類型,當(dāng)?shù)竭_(dá)超時(shí)時(shí)間后返回的信息。

用 TimeoutHandler 來(lái)封裝我們的 slowHandler,我們只需要:

package main  import (  "fmt"  "io"  "net/http"  "time" )  func slowHandler(w http.ResponseWriter, req *http.Request) {  time.Sleep(2 * time.Second)  io.WriteString(w, "I am slow!\n") }  func main() {  srv := http.Server{   Addr:         ":8888",   WriteTimeout: 5 * time.Second,   Handler:      http.TimeoutHandler(http.HandlerFunc(slowHandler), 1*time.Second, "Timeout!\n"),  }   if err := srv.ListenAndServe(); err != nil {   fmt.Printf("Server failed: %s\n", err)  } }

兩個(gè)需要留意的地方是:

  • 我們?cè)?http.TimetoutHandler 里封裝 slowHanlder,超時(shí)時(shí)間設(shè)為 1s,超時(shí)信息為 “Timeout!”。

  • 我們把 WriteTimeout 增加到 5s,以給予 http.TimeoutHandler 足夠的時(shí)間執(zhí)行。如果我們不這么做,當(dāng)  TimeoutHandler 開始執(zhí)行時(shí),已經(jīng)過(guò)了 deadline,不能再寫到響應(yīng)。

如果我們?cè)賳?dòng)服務(wù),當(dāng)程序運(yùn)行到 slow handler 時(shí),會(huì)有如下輸出:

$ time curl localhost:8888 Timeout! curl localhost:8888  0.01s user 0.01s system 1% CPU 1.023 total

1s 后,我們的 TimeoutHandler 開始執(zhí)行,阻止運(yùn)行 slowHandler,返回文本信息  ”Timeout!“。如果我們?cè)O(shè)置信息為空,handler 會(huì)返回默認(rèn)的超時(shí)響應(yīng)信息,如下:

<html>   <head>     <title>Timeout</title>   </head>   <body>    <h2>Timeout</h2>   </body> </html>

如果忽略掉輸出,這還算是整潔,不是嗎?現(xiàn)在我們的程序不會(huì)有過(guò)長(zhǎng)耗時(shí)的處理;也避免了有人惡意發(fā)送導(dǎo)致長(zhǎng)耗時(shí)處理的請(qǐng)求時(shí),導(dǎo)致的潛在的 DoS 攻擊。

盡管我們?cè)O(shè)置超時(shí)時(shí)間是一個(gè)偉大的開始,但它仍然只是初級(jí)的保護(hù)。如果你可能會(huì)面臨 DoS  攻擊,你應(yīng)該采用更高級(jí)的保護(hù)工具和技術(shù)。(可以試試Cloudflare[7] )

我們的 slowHandler 僅僅是個(gè)簡(jiǎn)單的 demo。但是,如果我們的程序復(fù)雜些,能向其他服務(wù)和資源發(fā)出請(qǐng)求會(huì)發(fā)生什么呢?如果我們的程序在超時(shí)時(shí)向諸如  S3 的服務(wù)發(fā)出了請(qǐng)求會(huì)怎么樣?

會(huì)發(fā)生什么?

未處理的超時(shí)和請(qǐng)求取消

我們稍微展開下我們的例子:

func slowAPICall() string {  d := rand.Intn(5)  select {  case <-time.After(time.Duration(d) * time.Second):   log.Printf("Slow API call done after %s seconds.\n", d)   return "foobar"  } }  func slowHandler(w http.ResponseWriter, r *http.Request) {  result := slowAPICall()  io.WriteString(w, result+"\n") }

我們假設(shè)最初我們不知道 slowHandler 由于通過(guò) slowAPICall 函數(shù)向 API 發(fā)請(qǐng)求導(dǎo)致需要耗費(fèi)這么長(zhǎng)時(shí)間才能處理完成,

slowAPICall 函數(shù)很簡(jiǎn)單:使用 select 和一個(gè)能阻塞 0 到 5 秒的 time.After 。當(dāng)經(jīng)過(guò)了阻塞的時(shí)間后,time.After  方法通過(guò)它的 channel 發(fā)送一個(gè)值,返回 "foobar" 。

(另一種方法是,使用 sleep(time.Duration(rand.Intn(5)) * time.Second),但我們?nèi)匀皇褂? select,因?yàn)樗鼤?huì)使我們下面的例子更簡(jiǎn)單。)

如果我們運(yùn)行起服務(wù),我們預(yù)期超時(shí) handler 會(huì)在 1 秒之后中斷請(qǐng)求處理。來(lái)發(fā)送一個(gè)請(qǐng)求驗(yàn)證一下:

$ time curl localhost:8888 Timeout! curl localhost:8888  0.01s user 0.01s system 1% CPU 1.021 total

通過(guò)觀察服務(wù)的輸出,我們會(huì)發(fā)現(xiàn),它是在幾秒之后打出日志的,而不是在超時(shí) handler 生效時(shí)打出:

$ Go run server.go 2019/12/29 17:20:03 Slow API call done after 4 seconds.

這個(gè)現(xiàn)象表明:雖然 1 秒之后請(qǐng)求超時(shí)了,但是服務(wù)仍然完整地處理了請(qǐng)求。這就是在 4 秒之后才打出日志的原因。

雖然在這個(gè)例子里問(wèn)題很簡(jiǎn)單,但是類似的現(xiàn)象在生產(chǎn)中可能變成一個(gè)嚴(yán)重的問(wèn)題。例如,當(dāng) slowAPICall  函數(shù)開啟了幾個(gè)百個(gè)協(xié)程,每個(gè)協(xié)程都處理一些數(shù)據(jù)時(shí)?;蛘弋?dāng)它向不同系統(tǒng)發(fā)出多個(gè)不同的 API  發(fā)出請(qǐng)求時(shí)。這種耗時(shí)長(zhǎng)的的進(jìn)程,它們的請(qǐng)求方/客戶端并不會(huì)使用服務(wù)端的返回結(jié)果,會(huì)耗盡你系統(tǒng)的資源。

所以,我們?cè)趺幢Wo(hù)系統(tǒng),使之不會(huì)出現(xiàn)類似的未優(yōu)化的超時(shí)或取消請(qǐng)求呢?

上下文超時(shí)和取消

Go 有一個(gè)包名為 `context`[8] 專門處理類似的場(chǎng)景。

context 包在 Go 1.7 版本中提升為標(biāo)準(zhǔn)庫(kù),在之前的版本中,以`golang.org/x/net/context`[9] 的路徑作為 Go  Sub-repository Packages[10]出現(xiàn)。

這個(gè)包定義了 Context 類型。它最初的目的是保存不同 API 和不同處理的截止時(shí)間、取消信號(hào)和其他請(qǐng)求相關(guān)的值。如果你想了解關(guān)于 context  包的其他信息,可以閱讀 Golang's blog[11] 中的 “Go 并發(fā)模式:Context”(譯注:Go Concurrency Patterns:  Context) .

net/http 包中的的 Request 類型已經(jīng)有 context 與之綁定。從 Go 1.7 開始,Request 新增了一個(gè)返回請(qǐng)求的上下文的  `Context` 方法[12]。對(duì)于進(jìn)來(lái)的請(qǐng)求,在客戶端關(guān)閉連接、請(qǐng)求被取消(HTTP/2 中)或 ServeHTTP  方法返回后,服務(wù)端會(huì)取消上下文。

我們期望的現(xiàn)象是,當(dāng)客戶端取消請(qǐng)求(輸入了 CTRL + C)或一段時(shí)間后TimeoutHandler  繼續(xù)執(zhí)行然后終止請(qǐng)求時(shí),服務(wù)端會(huì)停止后續(xù)的處理。進(jìn)而關(guān)閉所有的連接,釋放所有被運(yùn)行中的處理進(jìn)程(及它的所有子協(xié)程)占用的資源。

我們把 Context 作為參數(shù)傳給 slowAPICall 函數(shù):

func slowAPICall(ctx context.Context) string {  d := rand.Intn(5)  select {  case <-time.After(time.Duration(d) * time.Second):   log.Printf("Slow API call done after %d seconds.\n", d)   return "foobar"  } }  func slowHandler(w http.ResponseWriter, r *http.Request) {  result := slowAPICall(r.Context())  io.WriteString(w, result+"\n") }

在例子中我們利用了請(qǐng)求上下文,實(shí)際中怎么用呢?`Context` 類型[13]有個(gè) Done 屬性,類型為 <-chan  struct{}。當(dāng)進(jìn)程處理完成時(shí),Done 關(guān)閉,此時(shí)表示上下文應(yīng)該被取消,而這正是例子中我們需要的。

我們?cè)?slowAPICall 函數(shù)中用 select 處理 ctx.Done 通道。當(dāng)我們通過(guò) Done 通道接收一個(gè)空的 struct  時(shí),意味著上下文取消,我們需要讓 slowAPICall 函數(shù)返回一個(gè)空字符串。

func slowAPICall(ctx context.Context) string {  d := rand.Intn(5)  select {  case <-ctx.Done():   log.Printf("slowAPICall was supposed to take %s seconds, but was canceled.", d)   return ""         //time.After() 可能會(huì)導(dǎo)致內(nèi)存泄漏  case <-time.After(time.Duration(d) * time.Second):   log.Printf("Slow API call done after %d seconds.\n", d)   return "foobar"  } }

(這就是使用 select 而不是 time.Sleep -- 這里我們只能用 select 處理 Done 通道。)

在這個(gè)簡(jiǎn)單的例子中,我們成功得到了結(jié)果 -- 當(dāng)我們從 Done 通道接收值時(shí),我們打印了一行日志到 STDOUT  并返回了一個(gè)空字符串。在更復(fù)雜的情況下,如發(fā)送真實(shí)的 API 請(qǐng)求,你可能需要關(guān)閉連接或清理文件描述符。

我們?cè)賳?dòng)服務(wù),發(fā)送一個(gè) cRUL 請(qǐng)求:

# The cURL command: $ curl localhost:8888 Timeout!  # The server output: $ Go run server.go 2019/12/30 00:07:15 slowAPICall was supposed to take 2 seconds, but was canceled.

檢查輸出:我們發(fā)送了 cRUL 請(qǐng)求到服務(wù),它耗時(shí)超過(guò) 1 秒,服務(wù)取消了 slowAPICall  函數(shù)。我們幾乎不需要寫任何代碼。TimeoutHandler 為我們代勞了 -- 當(dāng)處理耗時(shí)超過(guò)預(yù)期時(shí),TimeoutHandler  終止了處理進(jìn)程并取消請(qǐng)求上下文。

TimeoutHandler 是在 `timeoutHandler.ServeHTTP` 方法[14] 中取消上下文的:

// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L3217-L3263 func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {         ctx := h.testContext         if ctx == nil {          var cancelCtx context.CancelFunc          ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)          defer cancelCtx()         }         r = r.WithContext(ctx)          // *Snipped* }

上面例子中,我們通過(guò)調(diào)用 context.WithTimeout 來(lái)使用請(qǐng)求上下文。超時(shí)值 h.dt (TimeoutHandler  的第二個(gè)參數(shù))設(shè)置給了上下文。返回的上下文是請(qǐng)求上下文設(shè)置了超時(shí)值后的一份拷貝。隨后,它作為請(qǐng)求上下文傳給r.WithContext(ctx)。

context.WithTimeout 方法執(zhí)行了上下文取消。它返回了 Context  設(shè)置了一個(gè)超時(shí)值之后的副本。當(dāng)?shù)竭_(dá)超時(shí)時(shí)間后,就取消上下文。

這里是執(zhí)行的代碼:

// Taken from: https://github.com/golang/go/blob/bbbc6589/src/context/context.go#L486-L498 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {  return WithDeadline(parent, time.Now().Add(timeout)) }  // Taken from: https://github.com/golang/go/blob/bbbc6589/src/context/context.go#L418-L450 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {         // *Snipped*          c := &timerCtx{          cancelCtx: newCancelCtx(parent),          deadline:  d,         }          // *Snipped*          if c.err == nil {          c.timer = time.AfterFunc(dur, func() {           c.cancel(true, DeadlineExceeded)          })         }         return c, func() { c.cancel(true, Canceled) } }

這里我們又看到了截止時(shí)間。WithDeadline 函數(shù)設(shè)置了一個(gè) d 到達(dá)之后執(zhí)行的函數(shù)。當(dāng)?shù)竭_(dá)截止時(shí)間后,它調(diào)用 cancel  方法處理上下文,此方法會(huì)關(guān)閉上下文的 done 通道并設(shè)置上下文的 timer 屬性為 nil。

Done 通道的關(guān)閉有效地取消了上下文,使我們的 slowAPICall 函數(shù)終止了它的執(zhí)行。這就是 TimeoutHandler  終止耗時(shí)長(zhǎng)的處理進(jìn)程的原理。

(如果你想閱讀上面提到的源碼,你可以去看 `cancelCtx` 類型[15] 和`timerCtx` 類型[16])

有彈性的 net/http 服務(wù)

連接截止時(shí)間提供了低級(jí)的細(xì)粒度控制。雖然它們的名字中含有“超時(shí)”,但它們并沒(méi)有表現(xiàn)出人們通常期望的“超時(shí)”。實(shí)際上它們非常強(qiáng)大,但是使用它們有一定的門檻。

另一個(gè)角度講,當(dāng)處理 HTTP 時(shí),我們?nèi)匀粦?yīng)該考慮使用 TimeoutHandler。Go  的作者們也選擇使用它,它有多種處理,提供了如此有彈性的處理以至于我們甚至可以對(duì)每一個(gè)處理使用不同的超時(shí)。TimeoutHandler  可以根據(jù)我們期望的表現(xiàn)來(lái)控制執(zhí)行進(jìn)程。

除此之外,TimeoutHandler 完美兼容 context 包。context  包很簡(jiǎn)單,包含了取消信號(hào)和請(qǐng)求相關(guān)的數(shù)據(jù),我們可以使用這些數(shù)據(jù)來(lái)使我們的應(yīng)用更好地處理錯(cuò)綜復(fù)雜的網(wǎng)絡(luò)問(wèn)題。

結(jié)束之前,有三個(gè)建議。寫 HTTP 服務(wù)時(shí),怎么設(shè)計(jì)超時(shí):

  • 最常用的,到達(dá) TimeoutHandler 時(shí),怎么處理。它進(jìn)行我們通常期望的超時(shí)處理。

  • 不要忘記上下文取消。context 包使用起來(lái)很簡(jiǎn)單,并且可以節(jié)省你服務(wù)器上的很多處理資源。尤其是在處理異?;蚓W(wǎng)絡(luò)狀況不好時(shí)。

  • 一定要用截止時(shí)間。確保做了完整的測(cè)試,驗(yàn)證了能提供你期望的所有功能。

感謝各位的閱讀,以上就是“怎么用取消參數(shù)使Go net/http服務(wù)更靈活”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)怎么用取消參數(shù)使Go net/http服務(wù)更靈活這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI