成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

圖文講透Golang標(biāo)準(zhǔn)庫(kù) net/http實(shí)現(xiàn)原理 - 客戶端

開發(fā) 前端
Client.Get() 根據(jù)用戶的入?yún)ⅲ?qǐng)求參數(shù) NewRequest使用上下文包裝NewRequestWithContext ,接著通過(guò) Client.Do 方法,處理這個(gè)請(qǐng)求。

客戶端的內(nèi)容將是如何發(fā)送請(qǐng)求和接收響應(yīng),走完客戶端就把整個(gè)流程就完整的串聯(lián)起來(lái)了!

這次我把調(diào)用的核心方法和流程走讀的函數(shù)也貼出來(lái),這樣看應(yīng)該更有邏輯感,重要部分用紅色標(biāo)記了一下,可以著重看下。

圖片圖片

先了解下核心數(shù)據(jù)結(jié)構(gòu)Client和Request。

Client結(jié)構(gòu)體

type Client struct { 
    Transport RoundTripper 
    CheckRedirect func(req *Request, via []*Request) error 
    Jar CookieJar 
    Timeout time.Duration
}

四個(gè)字段分別是:

  • ? Transport:表示 HTTP 事務(wù),用于處理客戶端的請(qǐng)求連接并等待服務(wù)端的響應(yīng);
  • ? CheckRedirect:處理重定向的策略
  • ? Jar:管理和存儲(chǔ)請(qǐng)求中的 cookie
  • ? Timeout:超時(shí)設(shè)置

Request結(jié)構(gòu)體

Request字段較多,這里就列舉一下常見(jiàn)的一些字段

type Request struct {
    Method string
    URL *url.URL
    Header Header
    Body io.ReadCloser
    Host string
    Response *Response
    ...
}
  • ? Method:指定的HTTP方法(GET、POST、PUT等)
  • ? URL:請(qǐng)求路徑
  • ? Header:請(qǐng)求頭
  • ? Body:請(qǐng)求體
  • ? Host:服務(wù)器主機(jī)
  • ? Response:響應(yīng)參數(shù)

構(gòu)造請(qǐng)求

var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
    return DefaultClient.Get(url)
}

示例HTTP 的 Get方法會(huì)調(diào)用到 DefaultClient 的 Get 方法,,然后調(diào)用到 Client 的 Get 方法。

DefaultClient 是 Client 的一個(gè)空實(shí)例(跟DefaultServeMux有點(diǎn)子相似)

圖片圖片

Client.Get

func (c *Client) Get(url string) (resp *Response, err error) {
    req, err := NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    return c.Do(req)
}

func NewRequest(method, url string, body io.Reader) (*Request, error) {
    return NewRequestWithContext(context.Background(), method, url, body)
}

Client.Get() 根據(jù)用戶的入?yún)ⅲ?qǐng)求參數(shù) NewRequest使用上下文包裝NewRequestWithContext ,接著通過(guò) Client.Do 方法,處理這個(gè)請(qǐng)求。

NewRequestWithContext

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    ...
    // 解析url
    u, err := urlpkg.Parse(url)
    ...
    rc, ok := body.(io.ReadCloser)
    if !ok && body != nil {
        rc = ioutil.NopCloser(body)
    } 
    u.Host = removeEmptyPort(u.Host)
    req := &Request{
        ctx:        ctx,
        Method:     method,
        URL:        u,
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     make(Header),
        Body:       rc,
        Host:       u.Host,
    } 
    ...
    return req, nil
}

NewRequestWithContext 函數(shù)主要是功能是將請(qǐng)求封裝成一個(gè) Request 結(jié)構(gòu)體并返回,這個(gè)結(jié)構(gòu)體的名稱是req。

準(zhǔn)備發(fā)送請(qǐng)求

構(gòu)造好的Request結(jié)構(gòu)req,會(huì)傳入c.Do()方法。

我們看下發(fā)送請(qǐng)求過(guò)程調(diào)用了哪些方法,用下圖表示下

圖片圖片

?? 其實(shí)不管是Get還是Post請(qǐng)求的調(diào)用流程都是一樣的,只是對(duì)外封裝了Post和Get請(qǐng)求

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    ...
    for {
        ...
        resp, didTimeout, err = send(req, deadline)
        if err != nil {
            return nil, didTimeout, err
        }
    }
    ...
}
//Client 調(diào)用 Do 方法處理發(fā)送請(qǐng)求最后會(huì)調(diào)用到 send 函數(shù)中
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    resp, didTimeout, err = send(req, c.transport(), deadline)
    if err != nil {
        return nil, didTimeout, err
    }
    ...
    return resp, nil, nil
}

c.transport()方法是為了回去Transport的默認(rèn)實(shí)例 DefaultTransport ,我們看下DefaultTransport長(zhǎng)什么樣。

DefaultTransport

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: defaultTransportDialContext(&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }),
    ForceAttemptHTTP2:     true,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

可以根據(jù)需要建立網(wǎng)絡(luò)連接,并緩存它們以供后續(xù)調(diào)用重用,部分參數(shù)如下:

  • ? MaxIdleConns:最大空閑連接數(shù)
  • ? IdleConnTimeout:空閑連接超時(shí)時(shí)間
  • ? ExpectContinueTimeout:預(yù)計(jì)繼續(xù)超時(shí)

注意這里的RoundTripper是個(gè)接口,也就是說(shuō) Transport 實(shí)現(xiàn) RoundTripper 接口,該接口方法接收Request,返回Response。

RoundTripper

type RoundTripper interface { 
    RoundTrip(*Request) (*Response, error)
}

圖片圖片

雖然還沒(méi)看完后面邏輯,不過(guò)我們猜測(cè)RoundTrip方法可能是實(shí)際處理客戶端請(qǐng)求的實(shí)現(xiàn)。

我們繼續(xù)追下后面邏輯,看下是否能驗(yàn)證這個(gè)猜想。

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    ...
    resp, err = rt.RoundTrip(req)
    if err != nil {
        ...
    }
    ..
}

?? 你看send函數(shù)的第二個(gè)參數(shù)就是接口類型,調(diào)用層傳遞的Transport的實(shí)例DefaultTransport。

而rt.RoundTrip()方法的調(diào)用具體在net/http/roundtrip.go文件中,這也是RoundTrip接口的實(shí)現(xiàn),代碼如下:

func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}

Transport.roundTrip 方法概況來(lái)說(shuō)干了這些事:

  • ? 封裝請(qǐng)求transportRequest
  • ? 調(diào)用 Transport 的 getConn 方法獲取連接
  • ? 在獲取到連接后,調(diào)用 persistConn 的 roundTrip 方法等待請(qǐng)求響應(yīng)結(jié)果
func (t *Transport) roundTrip(req *Request) (*Response, error) {
    ...  
    for {
        ...
        // 請(qǐng)求封裝
        treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey} 
        cm, err := t.connectMethodForRequest(treq)
        if err != nil {
            ...
        } 
        // 獲取連接
        pconn, err := t.getConn(treq, cm)
        if err != nil {
            ...
        }
        
        // 等待響應(yīng)結(jié)果
        var resp *Response
        if pconn.alt != nil {
            t.setReqCanceler(cancelKey, nil) 
            resp, err = pconn.alt.RoundTrip(req)
        } else {
            resp, err = pconn.roundTrip(treq)
        }
        ...
    }
}

封裝請(qǐng)求transportRequeste沒(méi)啥好說(shuō)的,因?yàn)閠req被roundTrip修改,所以這里需要為每次重試重新創(chuàng)建。

獲取連接

獲取連接的方法是 getConn,這里代碼還是比較長(zhǎng)的,會(huì)有不同的兩種方式去獲取連接:

  1. 1. 調(diào)用 queueForIdleConn 排隊(duì)等待獲取空閑連接
  2. 2. 如果獲取空閑連接失敗,那么調(diào)用 queueForDial 異步創(chuàng)建一個(gè)新的連接,并通過(guò)channel來(lái)接收readdy信號(hào),來(lái)確認(rèn)連接是否構(gòu)造完成

圖片圖片

getConn

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    ...
    //  初始化wantConn結(jié)構(gòu)體
    w := &wantConn{
        cm:         cm,
        key:        cm.key(),
        ctx:        ctx,
        ready:      make(chan struct{}, 1),
        beforeDial: testHookPrePendingDial,
        afterDial:  testHookPostPendingDial,
    }
    ...
    // 獲取空閑連接
    if delivered := t.queueForIdleConn(w); delivered {
        ...
    }
 
    // 異步創(chuàng)建新連接
    t.queueForDial(w)
 
    select {
    // 阻塞等待獲取到連接完成
    case <-w.ready:
        ...
        return w.pc, w.err
    ...
}

queueForIdleConn獲取空閑連接

獲取成功

成功空閑獲取連接Conn流程如下圖

圖片圖片

  1. 1. 根據(jù)wantConn的key從 transport.idleConn 這個(gè)map中查找,看是否存不存在空閑的 connection 列表
  2. 2. 獲取到空閑的 connection 列表后,從列表中拿最后一個(gè) connection
  3. 3. 獲取到連接后會(huì)調(diào)用 wantConn.tryDeliver 方法將連接綁定到 wantConn 請(qǐng)求參數(shù)上

獲取失敗

圖片圖片

當(dāng)不存在該請(qǐng)求的 connection 列表,會(huì)將當(dāng)前 wantConn 加入到名稱為 idleConnWait 的等待空閑map中。

不過(guò)此時(shí)的idleConnWait這個(gè)map的值是個(gè)隊(duì)列

queueForIdleConn方法

從上面的兩張圖解中差不多能看出是如何獲取空閑連接和如何獲取失敗時(shí)如何做的了,這里也貼下代碼體驗(yàn)下,讓大家更清楚里面的實(shí)現(xiàn)邏輯。

//idleConn是map類型,指定key返回切片列表
idleConn     map[connectMethodKey][]*persistConn 
//idleConnWait,指定key返回隊(duì)列
idleConnWait map[connectMethodKey]wantConnQueue

這里將獲取空閑連接的代碼實(shí)現(xiàn)多進(jìn)行注釋,更好理解一些!

func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    //參數(shù)判斷
    if t.DisableKeepAlives {
        return false
    }

    if w == nil { 
        return false
    }
 
    // 計(jì)算空閑連接超時(shí)時(shí)間
    var oldTime time.Time
    if t.IdleConnTimeout > 0 {
        oldTime = time.Now().Add(-t.IdleConnTimeout)
    }
    //從idleConn根據(jù)w.key找對(duì)應(yīng)的persistConn 列表
    if list, ok := t.idleConn[w.key]; ok {
        stop := false
        delivered := false
        for len(list) > 0 && !stop {
            // 找到persistConn列表最后一個(gè)
            pconn := list[len(list)-1] 
            // 檢查這個(gè) persistConn 是不是過(guò)期
            tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
            if tooOld {
                //如果過(guò)期進(jìn)行異步清理
                go pconn.closeConnIfStillIdle()
            }
            // 該 persistConn 被標(biāo)記為 broken 或 閑置太久 continue
            if pconn.isBroken() || tooOld { 
                list = list[:len(list)-1]
                continue
            }
            // 嘗試將該 persistConn 寫入到 wantConn(w)中
            delivered = w.tryDeliver(pconn, nil)
            if delivered {
                // 寫入成功,將persistConn從空閑列表中移除
                if pconn.alt != nil { 
                } else { 
                    t.idleLRU.remove(pconn)
                    //缺省了最后一個(gè)conn
                    list = list[:len(list)-1]
                }
            }
            stop = true
        }
        //對(duì)被獲取連接后的列表進(jìn)行判斷
        if len(list) > 0 {
            t.idleConn[w.key] = list
        } else {
            // 如果該 key 對(duì)應(yīng)的空閑列表不存在,那么將該key從字典中移除
            delete(t.idleConn, w.key)
        }
        if stop {
            return delivered
        }
    } 
    // 如果找不到空閑的 persistConn
    if t.idleConnWait == nil {
        t.idleConnWait = make(map[connectMethodKey]wantConnQueue)
    }
    // 將該 wantConn添加到等待空閑idleConnWait中
    q := t.idleConnWait[w.key] 
    q.cleanFront()
    q.pushBack(w)
    t.idleConnWait[w.key] = q
    return false
}

我們知道了為找到的空閑連接會(huì)被放到空閑 idleConnWait 這個(gè)等待map中,最后會(huì)被Transport.tryPutIdleConn方法將pconne添加到等待新請(qǐng)求的空閑持久連接列表中。

queueForDial創(chuàng)建新連接

queueForDial意思是排隊(duì)等待撥號(hào),為什么說(shuō)是等帶呢,因?yàn)樽罱K的結(jié)果是在ready這個(gè)channel上進(jìn)行通知的。

流程如下圖:

圖片圖片

我們先看下Transport結(jié)構(gòu)體的這兩個(gè)map,名稱不一樣map的屬性和解釋都是一樣的,其中idleConnWait是在沒(méi)查找空閑連接的時(shí)候存放當(dāng)前連接的map。

而connsPerHostWait用在了創(chuàng)建新連接的地方,可以猜測(cè)一下創(chuàng)建新鏈接的地方就是將當(dāng)前的請(qǐng)求放入到 connsPerHostWait 等待map中。

// waiting getConns
idleConnWait map[connectMethodKey]wantConnQueue 
// waiting getConns
connsPerHostWait map[connectMethodKey]wantConnQueue

Transport.queueForDial

func (t *Transport) queueForDial(w *wantConn) {
    w.beforeDial()
    // 小于等于零,意思是限制,直接異步建立連接
    if t.MaxConnsPerHost <= 0 {
        go t.dialConnFor(w)
        return
    }
    ...
    //host建立的連接數(shù)沒(méi)達(dá)到上限,執(zhí)行異步建立連接
    if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
        if t.connsPerHost == nil {
            t.connsPerHost = make(map[connectMethodKey]int)
        }
        t.connsPerHost[w.key] = n + 1
        go t.dialConnFor(w)
        return
    }
    //進(jìn)入等待隊(duì)列
    if t.connsPerHostWait == nil {
        t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
    }
    q := t.connsPerHostWait[w.key]
    q.cleanFront()
    q.pushBack(w)
    t.connsPerHostWait[w.key] = q
}

在獲取不到空閑連接之后,會(huì)嘗試去建立連接:

  1. 1. queueForDial 方法的內(nèi)部會(huì)先校驗(yàn) MaxConnsPerHost 是否未設(shè)置和是否已達(dá)上限
  2. 1.
  1. 1. 檢驗(yàn)不通過(guò)則將當(dāng)前的請(qǐng)求放入到 connsPerHostWait 這個(gè)等待map中
  2. 2. 校驗(yàn)通過(guò)那么會(huì)異步的調(diào)用 dialConnFor 方法創(chuàng)建連接

??那會(huì)不會(huì)queueForDial方法中將idleConnWait和connsPerHostWait打包到等待空閑連接idleConn這個(gè)map中呢?

我們繼續(xù)看dialConnFor的實(shí)現(xiàn),它會(huì)給我們這個(gè)問(wèn)題的答案!

dialConnFor

func (t *Transport) dialConnFor(w *wantConn) {
    defer w.afterDial()
    //創(chuàng)建 persistConn
    pc, err := t.dialConn(w.ctx, w.cm)
    //綁定到 wantConn
    delivered := w.tryDeliver(pc, err)
    if err == nil && (!delivered || pc.alt != nil) {
        //綁定wantConn失敗
        //放到存放空閑連接idleConn的map中
        t.putOrCloseIdleConn(pc)
    }
    if err != nil {
        t.decConnsPerHost(w.key)
    }
}
  • ? dialConnFor 先調(diào)用 dialConn 方法創(chuàng)建 TCP 連接
  • ? 調(diào)用 tryDeliver 將連接綁定到 wantConn 上,綁定成功的話,就將該鏈接放到空閑連接的idleConn這個(gè)map中
  • ? 綁定失敗的話會(huì)調(diào)用decConnsPerHost方法,用遞減密鑰的每主機(jī)連接計(jì)數(shù)方式,繼續(xù)異步調(diào)用Transport.dialConnFor

我們可以追蹤下代碼會(huì)發(fā)現(xiàn)Transport.tryPutIdleConn() 方法就是將persistConn添加到等待的空閑持久連接列表中的實(shí)現(xiàn)。

Transport.dialConn創(chuàng)建連接

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
    pconn = &persistConn{
        t:             t,
        cacheKey:      cm.key(),
        reqch:         make(chan requestAndChan, 1),
        writech:       make(chan writeRequest, 1),
        closech:       make(chan struct{}),
        writeErrCh:    make(chan error, 1),
        writeLoopDone: make(chan struct{}),
    }
    ...
    // 創(chuàng)建 tcp 連接,給pconn.conn
    conn, err := t.dial(ctx, "tcp", cm.addr())
    if err != nil {
        return nil, wrapErr(err)
    }
    pconn.conn = conn
    ...
    //開啟兩個(gè)goroutine處理讀寫
    go pconn.readLoop()
    go pconn.writeLoop()
    return pconn, nil
}

?? 看完這個(gè)創(chuàng)建persistConn的代碼是不是心里仿佛懂了什么?

上述代碼中HTTP 連接的創(chuàng)建過(guò)程是建立 tcp 連接,然后為連接異步處理讀寫數(shù)據(jù),最后將創(chuàng)建好的連接返回。

我們可以看到創(chuàng)建的每個(gè)連接會(huì)分別創(chuàng)建兩個(gè)goroutine循環(huán)地進(jìn)行進(jìn)行讀寫的處理,這就是為什么我們連接能接受請(qǐng)求參數(shù)和處理請(qǐng)求的響應(yīng)的關(guān)鍵。

?? 這兩個(gè)協(xié)程功能是這樣的!

  1. 1. persisConn.writeLoop(),通過(guò) persistConn.writech 通道讀取到客戶端提交的請(qǐng)求,將其發(fā)送到服務(wù)端
  2. 2. persisConn.readLoop(),讀取來(lái)自服務(wù)端的響應(yīng),并添加到 persistConn.reqCh 通道中,給persistConn.roundTrip 方法接收

想看這兩個(gè)協(xié)程

等待響應(yīng)

persistConn 連接本身創(chuàng)建了兩個(gè)讀寫goroutine,而這兩個(gè)goroutine就是通過(guò)兩個(gè)channel進(jìn)行通信的。

這個(gè)通信就是在persistConn.roundTrip()方法中的進(jìn)行傳遞交互的,其中writech 是用來(lái)寫入請(qǐng)求數(shù)據(jù),reqch是用來(lái)讀取響應(yīng)數(shù)據(jù)。

圖片圖片

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    ...
    // 請(qǐng)求數(shù)據(jù)寫入到 writech channel中
    pc.writech <- writeRequest{req, writeErrCh, continueCh}

    // 接收響應(yīng)的channel
    resc := make(chan responseAndError)
    // 接收響應(yīng)的結(jié)構(gòu)體 requestAndChan 寫到 reqch channel中
    pc.reqch <- requestAndChan{
        req:        req.Request,
        cancelKey:  req.cancelKey,
        ch:         resc,
        ...
    }
    ...
    for {
        ...
        select { 
            // 接收到響應(yīng)數(shù)據(jù)
        case re := <-resc:
            ...
            // return響應(yīng)數(shù)據(jù)
            return re.res, nil
        ...
    }
}

1. 連接獲取到之后,會(huì)調(diào)用連接的 roundTrip 方法,將請(qǐng)求數(shù)據(jù)寫入到 persisConn.writech channel中,而連接 persisConn 中的協(xié)程 writeLoop() 接收到請(qǐng)求后就會(huì)處理請(qǐng)求

2. 響應(yīng)結(jié)構(gòu)體 requestAndChan 寫入到 persisConn.reqch 中

3. 通過(guò)readLoop 接受響應(yīng)數(shù)據(jù),然后讀取 resc channel 的響應(yīng)結(jié)果

4. 接受到響應(yīng)數(shù)據(jù)之后循環(huán)結(jié)束,連接處理完成

好了,net/http標(biāo)準(zhǔn)庫(kù)的客戶端構(gòu)造請(qǐng)求、發(fā)送請(qǐng)求、接受服務(wù)端的請(qǐng)求數(shù)據(jù)流程就講完了,看完之后是否意欲未盡呢?

還別說(shuō),小許也是第一次看是如何實(shí)現(xiàn)的,確實(shí)還是了解到了點(diǎn)東西呢!

責(zé)任編輯:武曉燕 來(lái)源: 小許code
相關(guān)推薦

2024-01-29 08:04:48

Golang標(biāo)準(zhǔn)庫(kù)服務(wù)端

2021-10-18 05:00:38

語(yǔ)言GoRequestHTTP

2021-05-07 15:28:03

Kafka客戶端Sarama

2022-02-20 23:15:46

gRPCGolang語(yǔ)言

2023-10-12 07:54:02

.NETXamarin框架

2009-08-18 15:43:56

ASP.NET生成客戶端腳本

2009-07-24 17:31:56

ASP.NET AJA

2020-03-24 15:15:29

HttpClientOkHttpJava

2021-08-01 23:18:21

Redis Golang命令

2024-05-09 08:30:57

OkHttpHTTP客戶端

2025-03-14 09:20:46

2024-10-10 15:54:44

.NET開源Redis

2023-10-11 07:00:44

高可用程序客戶端

2024-05-29 07:30:41

2022-04-20 08:32:09

RabbitMQ流控制

2009-02-04 17:39:14

ibmdwWebSphereDataPower

2011-08-17 10:10:59

2024-10-09 07:35:49

2021-09-22 15:46:29

虛擬桌面瘦客戶端胖客戶端

2009-10-15 10:46:03

PPC客戶端程序VB.NET創(chuàng)建
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 欧美性区 | 精品国产第一区二区三区 | 亚洲国产第一页 | 91极品尤物在线播放国产 | 成人a视频在线观看 | 91久色 | 国产精品明星裸体写真集 | 亚洲综合在 | 一区视频 | 日韩在线中文 | 国产精品不卡视频 | 亚洲精品在线免费 | 伊人电影院av | 麻豆久久久 | 亚洲免费视频在线观看 | 欧美视频 | 麻豆精品一区二区三区在线观看 | 欧美自拍日韩 | 久久综合九九 | 在线观看日本网站 | 成人免费视频一区二区 | 国产欧美一区二区精品忘忧草 | 国产精品久久久久久久久久 | 新疆少妇videos高潮 | 久久久久久蜜桃一区二区 | 精品久久久一区 | 精品国产免费人成在线观看 | 欧美一区二区三区电影 | 久久久久国产视频 | 久久99成人 | 久久久久久中文字幕 | 久久国产精品免费一区二区三区 | 欧美一级大黄 | 一区二区免费看 | 91久久精品| 久久久久成人精品亚洲国产 | 日韩在线小视频 | 99亚洲精品视频 | 在线视频 中文字幕 | 日韩在线日韩 | 国产成人a亚洲精品 |