<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端架構(gòu)師破局技能,NodeJS 落地 WebSocket 實(shí)踐

          共 12035字,需瀏覽 25分鐘

           ·

          2021-12-10 14:57

          作者:楊成功

          簡介:專注前端工程與架構(gòu)產(chǎn)出

          來源:SegmentFault  思否社區(qū)


          本文從網(wǎng)絡(luò)協(xié)議,技術(shù)背景,安全和生產(chǎn)應(yīng)用的方向,詳細(xì)介紹 WebSocket 在 Node.js 中的落地實(shí)踐。


          大綱預(yù)覽



          本文介紹的內(nèi)容包括以下方面:


          • 網(wǎng)絡(luò)協(xié)議進(jìn)化
          • Socket.IO?
          • ws 模塊實(shí)現(xiàn)
          • Express 集成
          • WebSocket 實(shí)例
          • 消息廣播
          • 安全與認(rèn)證
          • BFF 應(yīng)用


          網(wǎng)絡(luò)協(xié)議進(jìn)化



          HTTP 協(xié)議是前端最熟悉的網(wǎng)絡(luò)通信協(xié)議。我們通常的打開網(wǎng)頁,請求接口,都屬于 HTTP 請求。

          HTTP 請求的特點(diǎn)是:請求-> 響應(yīng)。客戶端發(fā)起請求,服務(wù)端收到請求后進(jìn)行響應(yīng),一次請求就完成了。也就是說,HTTP 請求必須由客戶端發(fā)起,服務(wù)端才能被動響應(yīng)。

          除此之外,發(fā)起 HTTP 請求之前,還需要通過三次握手建立 TCP 連接。HTTP/1.0 的特點(diǎn)是,每通信一次,都要經(jīng)歷 “三步走” 的過程 —— TCP 連接 -> HTTP 通信 -> 斷開 TCP 連接。

          這樣的每一次請求都是獨(dú)立的,一次請求完成連接就會斷開。

          HTTP1.1 對請求過程做了優(yōu)化。TCP 連接建立之后,我們可以進(jìn)行多次 HTTP 通信,等到一個時間段無 HTTP 請求發(fā)起 TCP 才會斷開連接,這就是 HTTP/1.1 帶來的長連接技術(shù)。

          但是即便如此,通信方式依然是客戶端發(fā)起,服務(wù)端響應(yīng),這個根本邏輯不會變。

          隨著應(yīng)用交互的復(fù)雜,我們發(fā)現(xiàn),有一些場景是必須要實(shí)時獲取服務(wù)端消息的。

          比如即時聊天,比如消息推送,用戶并不會主動發(fā)起請求,但是當(dāng)服務(wù)器有了新消息,客戶端需要立刻知道并且反饋給用戶。

          HTTP 不支持服務(wù)端主動推送,但是這些場景又急需解決方案,于是早期出現(xiàn)了輪詢(polling)。輪詢是客戶端定時向服務(wù)器發(fā)起請求,檢測服務(wù)端是否有更新,如果有則返回新數(shù)據(jù)。

          這種輪詢方式雖然簡單粗暴,但很顯然有兩個弊端:

          1. 請求消耗太大。客戶端不斷請求,浪費(fèi)流量和服務(wù)器資源,給服務(wù)器造成壓力。
          2. 不能保證及時??蛻舳诵枰胶饧皶r性和性能,請求間隔必然不能太小,因此會有延遲。

          隨著 HTML5 推出 WebSocket,即時通訊場景終于迎來了根本解決方案。WebSocket 是全雙工通信協(xié)議,當(dāng)客戶端與服務(wù)端建立連接之后,雙方可以互相發(fā)送數(shù)據(jù),這樣的話就不需要客戶端通過輪詢這種低效的方式獲取數(shù)據(jù),服務(wù)端有新消息直接推送給客戶端即可。

          傳統(tǒng) HTTP 連接方式如下:

          ## 普通連接
          http://localhost:80/test
          ## 安全連接
          https://localhost:80/test

          WebSocket 是另一種協(xié)議,連接方式如下:

          ## 普通連接
          ws://localhost:80/test
          ## 安全連接
          wss://localhost:80/test

          但是 WebSocket 也不是完全脫離 HTTP 的,若要建立 WebSocket 連接,則必須要客戶端主動發(fā)起一個建立連接的 HTTP 請求,連接成功之后客戶端與服務(wù)端才能進(jìn)行雙向通信。

          Socket.IO?



          提起用 Node.js 實(shí)現(xiàn) WebSocket,大家一定會想到一個庫:Socket.IO

          Socket.IO地址:https://socket.io/

          沒錯,Socket.IO 是目前 Node.js 在生產(chǎn)環(huán)境中開發(fā) WebSocket 應(yīng)用最好的選擇。它功能強(qiáng)大,高性能,低延遲,并且可以一步集成到 express 框架中。

          但是也許你不清楚,Socket.IO 并不是一個純粹的 WebSocket 框架。它是將 Websocket 和輪詢機(jī)制以及其它的實(shí)時通信方式封裝成了通用的接口,以實(shí)現(xiàn)更高效的雙向通信。

          嚴(yán)格來說,Websocket 只是 Socket.IO 的一部分。

          也許你會問:既然 Socket.IO 在 WebSocket 的基礎(chǔ)上做了那么多的優(yōu)化,并且非常成熟,那為什么還要搭一個原生 WebSocket 服務(wù)?

          首先,Socket.IO 不能通過原生的 ws 協(xié)議連接。比如你在瀏覽器試圖通過 ws://localhost:8080/test-socket 這種方式連接 Socket.IO 服務(wù),是連接不上的。因為 Socket.IO 的服務(wù)端必須通過 Socket.IO 的客戶端連接,不支持默認(rèn)的 WebSocket 方式連接。

          其次,Socket.IO 封裝程度非常高,使用它可能不利于你了解 WebSocket 建立連接的原理。

          因此,我們本篇就用 Node.js 中基礎(chǔ)的 ws 模塊,從頭開始實(shí)現(xiàn)一個原生的 WebSocket 服務(wù),并且在前端用 ws 協(xié)議直接連接,體驗一把雙向通信的感覺!

          ws 模塊實(shí)現(xiàn)



          ws 是 Node.js 下一個簡單快速,并且定制程度極高的 WebSocket 實(shí)現(xiàn)方案,同時包含了服務(wù)端和客戶端。

          ws地址:https://github.com/websockets/ws

          用 ws 搭建起來的服務(wù)端,瀏覽器可以通過原生 WebSocket 構(gòu)造函數(shù)直接連接,非常便捷。ws 客戶端則是模擬瀏覽器的 WebSocket 構(gòu)造函數(shù),用于連接其他 WebSocket 服務(wù)器進(jìn)行通信。

          注意一點(diǎn):ws 只能在 Node.js 環(huán)境中使用,瀏覽器中不可用,瀏覽器請直接使用原生 WebSocket 構(gòu)造函數(shù)。

          下面開始接入,第一步,安裝 ws:

          $ npm install ws

          安裝好后,我們先搭建一個 ws 服務(wù)端。

          服務(wù)端


          搭建 websocket 服務(wù)器需要用 WebSocketServer 構(gòu)造函數(shù)。

          const { WebSocketServer } = require('ws')
          const wss = new WebSocketServer({
            port: 8080
          })
          wss.on('connection', (ws, req) => {
            console.log('客戶端已連接:', req.socket.remoteAddress)
            ws.on('message', data => {
              console.log('收到客戶端發(fā)送的消息:', data)
            })
            ws.send('我是服務(wù)端') // 向當(dāng)前客戶端發(fā)送消息
          })

          把這段代碼寫進(jìn) ws-server.js 然后運(yùn)行:

          $ node ws-server.js

          這樣一個監(jiān)聽 8080 端口的 WebSocket 服務(wù)器就已經(jīng)跑起來了。


          客戶端


          上一步建好了 WebSocket 服務(wù)器,現(xiàn)在我們在前端連接并監(jiān)聽消息:

          var ws = new WebSocket('ws://localhost:8080')

          ws.onopen = function(mevt) {
            console.log('客戶端已連接')
          }
          ws.onmessage = function(mevt) {
            console.log('客戶端收到消息: ' + evt.data)
            ws.close()
          }
          ws.onclose = function(mevt) {
            console.log('連接關(guān)閉')
          }

          將代碼寫入 wsc.html 然后用瀏覽器打開,看到打印如下:


          可以看到,瀏覽器連接成功后,收到服務(wù)端主動推送過來的消息,然后瀏覽器可以主動關(guān)閉連接。

          Node.js 環(huán)境下我們看 ws 模塊如何發(fā)起連接:

          const WebSocket = require('ws')
          var ws = new WebSocket('ws://localhost:8080')

          ws.on('open', () => {
            console.log('客戶端已連接')
          })
          ws.on('message', data => {
            console.log('客戶端收到消息: ' + data)
            ws.close()
          })
          ws.on('close', () => {
            console.log('連接關(guān)閉')
          })

          代碼與瀏覽器的邏輯一摸一樣,只是寫法稍有些不同,注意區(qū)別。

          需要特殊說明的一點(diǎn),瀏覽器端監(jiān)聽 message 事件的回調(diào)函數(shù),參數(shù)是一個 MessageEvent 的實(shí)例對象,服務(wù)端發(fā)來的實(shí)際數(shù)據(jù)需要通過 mevt.data 獲取。

          MessageEvent地址:https://developer.mozilla.org/zh-CN/docs/Web/API/MessageEvent

          而在 ws 客戶端,這個參數(shù)就是服務(wù)端的實(shí)際數(shù)據(jù),直接獲取即可。

          Express 集成



          ws 模塊一般不會單獨(dú)使用,更優(yōu)的方案是集成到現(xiàn)有的框架中。這節(jié)我們將 ws 模塊集成到 Expre?ss 框架。

          Express地址:https://expressjs.com/zh-cn/

          集成到 Express 框架的優(yōu)點(diǎn)是,我們不需要單獨(dú)監(jiān)聽一個端口,使用框架啟動的端口即可,并且我們還可以指定訪問到某個路由,才發(fā)起 WebSocket 連接。

          幸運(yùn)的是這一切不需要手動實(shí)現(xiàn),express-ws 模塊已經(jīng)幫我們做好了大部分的集成工作。

          express-ws地址:https://github.com/HenningM/express-ws

          首先安裝,然后在入口文件引入:

          var expressWs = require('express-ws')(app)

          和 Express 的 Router 一樣,express-ws 也支持注冊全局路由和局部路由。

          先看全局路由,通過 [host]/test-ws 連接:

          app.ws('/test-ws', (ws, req) => {
            ws.on('message', msg => {
              ws.send(msg)
            })
          })

          局部路由則是注冊在一個路由組下面的子路由。配置一個名為 websocket 的路由組并指向 websocket.js 文件,代碼如下:

          // websocket.js
          var router = express.Router()

          router.ws('/test-ws', (ws, req) => {
            ws.on('message', msg => {
              ws.send(msg)
            })
          })

          module.exports = router

          連接 [host]/websocket/test-ws 就可以訪問到這個子路由。

          路由組的作用是定義一個 websocket 連接組,不同需求連接這個組下的不同子路由。比如可以將 單聊 和 群聊 設(shè)置為兩個子路由,分別處理各自的連接通信邏輯。

          完整代碼如下:

          var express = require('express')
          var app = express()
          var wsServer = require('express-ws')(app)
          var webSocket = require('./websocket.js')

          app.ws('/test-ws', (ws, req) => {
            ws.on('message', msg => {
              ws.send(msg)
            })
          })

          app.use('/websocket', webSocket)

          app.listen(3000)

          實(shí)際開發(fā)中獲取常用信息的小方法:

          // 客戶端的IP地址
          req.socket.remoteAddress
          // 連接參數(shù)
          req.query

          WebSocket 實(shí)例



          WebSocket 實(shí)例是指客戶端連接對象,以及服務(wù)端連接的第一個參數(shù)。

          var ws = new WebSocket('ws://localhost:8080')
          app.ws('/test-ws', (ws, req) => {}

          代碼中的 ws 就是 WebSocket 實(shí)例,表示建立的連接。


          瀏覽器


          瀏覽器的 ws 對象中包含的信息如下:

          {
            binaryType: 'blob'
            bufferedAmount: 0
            extensions: ''
            onclose: null
            onerror: null
            onmessage: null
            onopen: null
            protocol: ''
            readyState: 3
            url: 'ws://localhost:8080/'
          }

          首先非常關(guān)鍵的是四個監(jiān)聽屬性,用于定義函數(shù):

          • onopen:連接建立后的函數(shù)
          • onmessage:收到服務(wù)端推送消息的函數(shù)
          • onclose:連接關(guān)閉的函數(shù)
          • onerror:連接異常的函數(shù)

          其中最常用的是 onmessage 屬性,賦值為一個函數(shù)來監(jiān)聽服務(wù)端消息:

          ws.onmessage = mevt => {
            console.log('消息:', mevt.data)
          }

          還有一個關(guān)鍵屬性是 readyState,表示連接狀態(tài),值為一個數(shù)字。并且每個值都可以用常量表示,對應(yīng)關(guān)系和含義如下:

          • 0: 常量 WebSocket.CONNECTING,表示正在連接
          • 1: 常量 WebSocket.OPEN,表示已連接
          • 2: 常量 WebSocket.CLOSING,表示正在關(guān)閉
          • 3: 常量 WebSocket.CLOSED,表示已關(guān)閉

          當(dāng)然最重要的還有 send 方法用于發(fā)送信息,向服務(wù)端發(fā)送數(shù)據(jù):

          ws.send('要發(fā)送的信息')

          服務(wù)端


          服務(wù)端的 ws 對象表示當(dāng)前發(fā)起連接的一個客戶端,基本屬性與瀏覽器大致相同。

          比如上面客戶端的四個監(jiān)聽屬性,readyState 屬性,以及 send 方法都是一致的。不過因為服務(wù)端是 Node.js 實(shí)現(xiàn),因此會有更豐富的支持。

          比如下面兩種監(jiān)聽事件的寫法效果是一樣的:

          // Node.js 環(huán)境
          ws.onmessage = str => {
            console.log('消息:', str)
          }
          ws.on('message', str => {
            console.log('消息:', mevt.data)
          })

          詳細(xì)的屬性和介紹可以查閱官方文檔

          地址:https://github.com/websockets/ws/blob/master/doc/ws.md#class-websocket


          消息廣播



          WebSocket 服務(wù)器不會只有一個客戶端連接,消息廣播的意思就是把信息發(fā)給所有已連接的客戶端,像一個大喇叭一樣,所有人都聽得到,經(jīng)典場景就是熱點(diǎn)推送。

          那么廣播之前,就必須要解決一個問題,如何獲取當(dāng)前已連接(在線)的客戶端?

          其實(shí) ws 模塊提供了快捷的獲取方法:

          var wss = new WebSocketServer({ port: 8080 })
          // 獲取所有已連接客戶端
          wss.clients

          方便吧。再看 express-ws 怎么獲?。?/span>

          var wsServer = expressWebSocket(app)
          var wss = wsServer.getWss()
          // 獲取所有已連接客戶端
          wss.clients

          拿到 wss.clients 后,我們看看它到底是什么樣子。經(jīng)過打印,發(fā)現(xiàn)它的數(shù)據(jù)結(jié)構(gòu)比想象到還要簡單,就是由所有在線客戶端的 WebSocket 實(shí)例組成的一個 Set 集合。

          那么,獲取當(dāng)前在線客戶端的數(shù)量:

          wss.clients.size

          簡單粗暴的實(shí)現(xiàn)廣播:

          wss.clients.forEach(client => {
            if (client.readyState === 1) {
              client.send('廣播數(shù)據(jù)')
            }
          })

          這是非常簡單,基礎(chǔ)的實(shí)現(xiàn)方式。試想一下如果此刻在線客戶有 10000 個,那么這個循環(huán)多半會卡死吧。因此才會有像 socket.io 這樣的庫,對基礎(chǔ)功能做了大量優(yōu)化和封裝,提高并發(fā)性能。

          上面的廣播屬于全局廣播,就是將消息發(fā)給所有人。然而還有另一種場景,比如一個 5 人的群聊小組聊天,這時的廣播只是給這 5 人小團(tuán)體發(fā)消息,因此這也叫 局部廣播。

          局部廣播的實(shí)現(xiàn)要復(fù)雜一些,一般會揉合具體的業(yè)務(wù)場景。這就需要我們在客戶端連接時,對客戶端數(shù)據(jù)做持久化處理了。比如用 Redis 存儲在線客戶端的狀態(tài)和數(shù)據(jù),這樣檢索分類更快,效率更高。

          局部廣播實(shí)現(xiàn),那一對一私聊就更容易了。找到兩個客戶端對應(yīng)的 WebSocket 實(shí)例互發(fā)消息就行。

          安全與認(rèn)證



          前面搭建好的 WebSocket 服務(wù)器,默認(rèn)任何客戶端都可以連接,這在生產(chǎn)環(huán)境肯定是不行的。我們要對 WebSocket 服務(wù)器做安全保障,主要是從兩個方面入手:

          1. Token 連接認(rèn)證
          2. wss 支持

          下面說一說我的實(shí)現(xiàn)思路。

          Token 連接認(rèn)證


          HTTP 請求接口我們一般會做 JWT 認(rèn)證,在請求頭中帶一個指定 Header,將一個 token 字符串傳過去,后端會拿這個 token 做校驗,校驗失敗則返回 401 錯誤阻止請求。

          JWT地址:https://jwt.io/

          我們上面說過,WebSocket 建立連接的第一步是客戶端發(fā)起一個 HTTP 的連接請求,那么我們在這個 HTTP 請求上做驗證,如果驗證失敗,則中段 WebSocket 的連接創(chuàng)建,不就可以了?

          順著這個思路,我們來改造一下服務(wù)端代碼。

          因為要在 HTTP 層做校驗,所以用 http 模塊創(chuàng)建服務(wù)器,關(guān)掉 WebSocket 服務(wù)的端口。

          var server = http.createServer()
          var wss = new WebSocketServer({ noServer: true })

          server.listen(8080)

          當(dāng)客戶端通過 ws:// 連接服務(wù)端時,服務(wù)端會進(jìn)行協(xié)議升級,也就是將 http 協(xié)議升級成 websocket 協(xié)議,此時會觸發(fā) upgrade 事件:

          server.on('upgrade', (request, socket) => {
            // 用 request 獲取參數(shù)做驗證
            // 1. 驗證不通過判斷
            if ('驗證失敗') {
              socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
              socket.destroy()
              return
            }
            // 2. 驗證通過,繼續(xù)建立連接
            wss.handleUpgrade(request, socket, _, ws => {
              wss.emit('connection', ws, request)
            })
          })

          // 3. 監(jiān)聽連接
          wss.on('connection', (ws, request) => {
            console.log('客戶端已連接')
            ws.send('服務(wù)端信息')
          })

          這樣服務(wù)端認(rèn)證添加完畢,具體的認(rèn)證方法結(jié)合客戶端的傳參方式來定。

          WebSocket 客戶端連接不支持自定義 Header,因此不能用 JWT 的方案,可用方案有兩種:

          • Basic Auth
          • Quary 傳參

          Basic Auth 認(rèn)證簡單說就是賬號+密碼認(rèn)證,而且賬號密碼是帶在 URL 里的。
          假設(shè)我有賬號是 ruims,密碼是 123456,那么客戶端連接是這樣:

          var ws = new WebSocket('ws://ruims:123456@localhost:8080')

          那么服務(wù)端就會收到這樣一個請求頭:

          wss.on('connection', (ws, req) => {
            if(req.headers['authorization']) {
              let auth = req.headers['authorization']
              console.log(auth)
              // 打印的值:Basic cnVpbXM6MTIzNDU2
            }
          }

          其中 cnVpbXM6MTIzNDU2 就是 ruims:123456 的 base64 編碼,服務(wù)端可以獲取到這個編碼來做認(rèn)證。

          Quary 傳參比較簡單,就是普通的 URL 傳參,可以帶一個短一點(diǎn)的加密字符串過去,服務(wù)端獲取到該字符串然后做認(rèn)證:

          var ws = new WebSocket('ws://localhost:8080?token=cnVpbXM6MTIzNDU2')


          服務(wù)端獲取參數(shù):

          wss.on('connection', (ws, req) => {
            console.log(req.query.token)
          }

          wss 支持


          WebSocket 客戶端使用 ws:// 協(xié)議連接,那 wss 是什么意思?

          其實(shí)非常簡單,和 https 原理一摸一樣。

          https 表示安全的 http 協(xié)議,組成是 HTTP + SSL

          wss 則表示安全的 ws 協(xié)議,組成是 WS + SSL

          那為什么一定要用 wss 呢?除了安全性,還有一個關(guān)鍵原因是:如果你的 web 應(yīng)用是 https 協(xié)議,你在當(dāng)前應(yīng)用中使用 WebSocket 就必須是 wss 協(xié)議,否則瀏覽器拒絕連接。

          配置 wss 直接在 https 配置中加一個 location 即可,直接上 nginx 配置:

          location /websocket {
            proxy_pass http://127.0.0.1:8080;
            proxy_redirect off;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection upgrade;
          }

          然后客戶端連接就變成了這樣:

          var ws = new WebSocket('wss://[host]/websocket')

          BFF 應(yīng)用



          BFF 或許你聽說過,全稱是 Backend For Frontend,意思是為前端服務(wù)的后端,在實(shí)際應(yīng)用架構(gòu)中屬于前端和后端的一個 中間層。


          這個中間層一般是由 Node.js 實(shí)現(xiàn),那么它有什么作用呢?

          眾所周知,現(xiàn)在后端的主流架構(gòu)是微服務(wù),微服務(wù)情況下 API 會劃分的非常細(xì),商品服務(wù)就是商品服務(wù),通知服務(wù)就是通知服務(wù)。當(dāng)你想在商品上架時給用戶發(fā)一個通知,可能至少需要調(diào)兩個接口。

          這樣的話對前端其實(shí)是不友好的,于是后來出現(xiàn)了 BFF 中間層,相當(dāng)于一個后端請求的中間代理站,前端可以直接請求 BFF 的接口,然后 BFF 再向后端接口請求,將需要的數(shù)據(jù)組合起來,一次返回前端。

          那我們在上面講的一大堆 WebSocket 的知識,在 BFF 層如何應(yīng)用呢?

          我想到的應(yīng)用場景至少有 4 個:

          1. 查看當(dāng)前在線人數(shù),在線用戶信息
          2. 登錄新設(shè)備,其他設(shè)備退出登錄
          3. 檢測網(wǎng)絡(luò)連接/斷開
          4. 站內(nèi)消息,小圓點(diǎn)提示

          這些功能以前是在后端實(shí)現(xiàn)的,并且會與其他業(yè)務(wù)功能耦合?,F(xiàn)在有了 BFF,那么 WebSocket 完全可以在這一層實(shí)現(xiàn),讓后端可以專注核心數(shù)據(jù)邏輯。

          由此可見,掌握了 WebSocket 在 Node.js 中的實(shí)踐應(yīng)用,作為前端的我們可以破除內(nèi)卷,在另一個領(lǐng)域繼續(xù)發(fā)揮價值,豈不美哉?

          源碼+答疑



          本文所有的代碼都是經(jīng)過我親自實(shí)踐,為了便于小伙伴們查閱和試驗,我建了一個 GitHub 倉庫專門存放本文的完整源碼,以及之后文章的完整源碼。

          倉庫地址在這里:https://github.com/ruidoc/blog-codes



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 81
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.欧美成人 | AV天堂电影在线 | 潮喷在线 | 亚洲在线观看免费视频 | 欧美日比视频在线观看 |