起因:項目首頁左右兩欄布局,左側布局是一個列表始終固定,右側布局路由跳轉,左側列表定時刷新(http輪訓),右側路由跳轉時會有一些頁面初始化的請求和用戶點擊交互的請求。
目前很多定時刷新,都是http輪訓,方便且快捷。但我這種情況有一個問題,定時器1s刷新請求接口,如果此時再操作右側面板,會導致有請求失敗的情況,
因為1s定時刷新占用帶寬,會導致其它的請求阻斷。(ps:曾經對於定時刷新做過一個優化,即在發下一個請求前,取消上次的http請求,但感覺還是很蹩腳,感興趣的可以去研究下)
基於上述情況,最后采用websocket實時推送,在介紹使用前,有一些關於websocket特性在此說明下
websocket特性:
一、主要有ws(不加密)和wss(加密);ws是基於http請求建立握手,wss是基於https請求建立握手
二、ws的握手基於http的get方式,協議應不小於1.1
二、ws和wss的請求頭會比單純的http的請求頭多很多特殊的header
三、ws請求在建立連接后,通信雙方都可以在任何時刻向另一方發送數據(http只能客戶端發送請求給服務端)
websocket的基本用法這里就不介紹了,網上相關的文章有很多,在此貼出不錯的鏈接地址
w3c對websocket的介紹:https://www.runoob.com/html/html5-websocket.html
阮一峰大師的帖子:http://www.ruanyifeng.com/blog/2017/05/websocket.html
項目環境:vue nginx webpack
本地跑項目(http://localhost:8080/),項目地址是http,可以發送ws請求,本地使用webpack代理
// 修改vue.config.js文件 devServer: { open: true, // 啟動后在瀏覽器打開 proxy: { '/api': {// 設置普通的http代理 target: 'http://x.x.x.x:8080', changeOrigin: true, pathRewrite: { '^/api': '' } }, '/socket': {// 設置websocket代理 target: 'http://x.x.x.x:8080', ws: true, // 開啟websocket代理 注意 changeOrigin: true, pathRewrite: { '^/socket': '' } }}}
本地開發到這里一切都很順利,接下來需要線上測試環境代理websocket,線上環境使用nginx代理,如下修改nginx配置
http { ... map $http_upgrade $connection_upgrade { default upgrade; '' close; } ... server { listen 8084; server_name _; root /usr/share/nginx/html/pcNet; include /etc/nginx/default.d/*.conf; location ^~/socket/ { proxy_pass http://x.x.x.x:8080/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade websocket; proxy_set_header Connection Upgrade; } location ^~/api/ { proxy_pass http://x.x.x.x:8080/; } ... }}
配置的屬性參數:
map指令:根據變量$http_upgrade的值創建新的變量$connection_upgrade,創建規則就是{}里面的東西,若規則沒有做匹配,則$connection_upgrade為默認值upgrade,若$http_upgrade為空字符串的話,
則$connection_upgrade值會是 close。
proxy_pass:
要代理到的url
roxy_http_version:
代理時使用的 http版本
proxy_set_header Host:http請求的主機域名
proxy_set_header X-Real-IP:
給代理設置原http請求的ip,填寫$remote_addr
即可
proxy_set_header X-Forwarded-For:反向代理之后轉發之前的IP地址
proxy_set_header Upgrade:
把代理時http請求頭的Upgrade
設置為原來http請求的請求頭,wss協議的請求頭為websocket
proxy_set_header Connection:
因為代理的wss協議,所以http請求頭的Connection
設置為Upgrade
nginx代理設置好后,線上訪問控制台報錯:
上圖的意思是在https的項目里需要發起wss請求,故如下修改:
到這里,客戶端和服務器已能通訊。
在此需要特別注意:https是加密請求,需要SSL加密,nginx配置https的代理(wss的握手階段是https請求)
這個是比較麻煩的,在此貼出解決方案的鏈接:https://www.cnblogs.com/zhoudawei/p/9257276.html
本文暫不介紹如何配置SSL,因為本項目的線上環境,網關做了攔截,會把https請求替換為http,wss請求替換為ws,這一點就省去申請CA證書和配置nginx支持https的麻煩
經過上面的配置,我們的websocket在nginx上跑起來了,萬分歡喜的我們,發現一分鍾不發消息就自動短線了。
造成原因:默認情況下,如果代理服務器nginx在60s內沒有傳輸任何數據,則連接將被關閉。
解決方案:nginx給出兩種解決方案:
1.修改 proxy_read_timeout(默認60秒)https://www.iteye.com/blog/happyqing-2320095
2.客戶端瀏覽器定時發送心跳包(時間要短於proxy_read_timeout)。
第一種簡單粗暴(我試了還沒有成功,還請各位大神賜教),但是時間再長也是一個值,還是會有超時的可能,基於此我使用第二種方案,在60s內定時發送心跳包來重置保持連接的時間。
vue單頁面代碼如下:
export default { data () { return { socket: null, connectCount: 0, // 連接次數 heartInterval: null } }, created () { this.initSocket() }, methods: { /** * 建立連接是http * 消息推送都是tcp連接,沒有同源限制 * 服務端人員try catch 消息推送不成功,則關閉連接 */ initSocket () { // 建立連接 (線上環境) const url = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/socket` this.socket = new WebSocket(`${url}/meeting/wsServer/PC-${this.$store.getters.userId}`) this.socket.onmessage = (evt) => { if (evt.data === '連接成功' || evt.data.includes('refresh')) { this.heartCheck() // 重置心跳檢測 this.onRefresh() // 接收到推送消息,刷新列表 } } // 監聽窗口事件,當窗口關閉時,主動斷開websocket連接 window.onbeforeunload = () => { this.socket.close() this.heartInterval && clearTimeout(this.heartInterval) } }, /** * 定時發送心跳包 * 59s發送一次心跳,比nginx設置的最大連接時間短一點,以達到在臨界點重置連接時間 */ heartCheck () { const that = this this.heartInterval && clearTimeout(this.heartInterval) this.heartInterval = setInterval(() => { if (this.socket.readyState === 1) { // 連接狀態 this.socket.send('ping') } else { that.connectCount += 1 if (that.connectCount <= 5) { this.initSocket() // 斷點重連5次 } } }, 59 * 1000) } } }
ps:當客戶端網絡不好,或者斷網,服務器並不知情,還是會給客戶端推送消息,造成資源浪費,故后端服務器要做異常處理,在消息推送不成功,主動關閉websocket連接。
在此遇到一個問題,所有問題都解決了,但是始終建立握手沒成功,后經排查是后端對ws的請求也做了登錄攔截,檢測沒有用戶token,給拒絕了
然后把ws的接口去除登錄攔截,就調通了。